diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e7714ae --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include banking_breakdown/ui/main_window.ui +include banking_breakdown/ui/warning.png +include banking_breakdown/document_builder/.latexmkrc +include banking_breakdown/document_builder/Dockerfile.alpine +include banking_breakdown/document_builder/report.tex +include banking_breakdown/document_builder/common.tex diff --git a/banking_breakdown/__main__.py b/banking_breakdown/__main__.py index 4ca743c..8f52710 100644 --- a/banking_breakdown/__main__.py +++ b/banking_breakdown/__main__.py @@ -20,7 +20,7 @@ def categorize_func(args): def report_func(args): report_data = statement_parser.parse_statement(args.i) - document_builder.build_document(report_data) + document_builder.build_document(args.o, report_data) # @@ -48,7 +48,10 @@ def main(): report_parser = subparsers.add_parser("report") report_parser.set_defaults(func=report_func) report_parser.add_argument('-i', required=True, - help="CSV output file from categorization step") + help="CSV file containing bank statement data (output file of categorization step)") + report_parser.add_argument('-o', required=False, + help="Output directory ('build' by default)", + default='build') args = parser.parse_args() diff --git a/banking_breakdown/document_builder.py b/banking_breakdown/document_builder.py deleted file mode 100644 index 7bf14fc..0000000 --- a/banking_breakdown/document_builder.py +++ /dev/null @@ -1,38 +0,0 @@ -import subprocess -import os -import shutil -from banking_breakdown import types - - -def _copy_resources(): - os.makedirs(os.path.dirname("build/report.tex"), exist_ok=True) - shutil.copyfile("res/report.tex", "build/report.tex") - shutil.copyfile("res/.latexmkrc", "build/.latexmkrc") - - -def _serialize_report_data(report_data: types.ReportData): - report_data.net_income.to_csv('build/net_income.csv', index=False) - report_data.category_overview.to_csv('build/category_overview.csv', - index=False) - report_data.expenses_by_category.to_csv('build/expenses_by_category.csv', - index=False) - report_data.total_value.to_csv('build/total_value.csv', index=False) - report_data.detailed_balance.to_csv('build/detailed_balance.csv', - index=False) - - -def _compile_document(): - subprocess.call("docker build . -f res/Dockerfile.alpine" - " -t banking-breakdown", - shell=True) - subprocess.call("docker run --rm -u `id -u`:`id -g`" - " -v $(realpath .):/project" - " -w /project/build banking-breakdown" - " latexmk -pdf report.tex", - shell=True) - - -def build_document(report_data: types.ReportData): - _copy_resources() - _serialize_report_data(report_data) - _compile_document() diff --git a/res/.latexmkrc b/banking_breakdown/document_builder/.latexmkrc similarity index 100% rename from res/.latexmkrc rename to banking_breakdown/document_builder/.latexmkrc diff --git a/res/Dockerfile.alpine b/banking_breakdown/document_builder/Dockerfile.alpine similarity index 100% rename from res/Dockerfile.alpine rename to banking_breakdown/document_builder/Dockerfile.alpine diff --git a/banking_breakdown/document_builder/__init__.py b/banking_breakdown/document_builder/__init__.py new file mode 100644 index 0000000..334939a --- /dev/null +++ b/banking_breakdown/document_builder/__init__.py @@ -0,0 +1,48 @@ +import subprocess +import os +import shutil +from banking_breakdown import types + +import banking_breakdown.document_builder + +from importlib import resources + + +def _copy_resources(build_dir: str): + res_prefix = resources.files(banking_breakdown.document_builder) + + os.makedirs(os.path.dirname(f"{build_dir}/report.tex"), exist_ok=True) + + shutil.copyfile(f"{res_prefix}/report.tex", f"{build_dir}/report.tex") + shutil.copyfile(f"{res_prefix}/common.tex", f"{build_dir}/common.tex") + shutil.copyfile(f"{res_prefix}/.latexmkrc", f"{build_dir}/.latexmkrc") + + +def _serialize_report_data(build_dir: str, report_data: types.ReportData): + report_data.net_income.to_csv(f'{build_dir}/net_income.csv', index=False) + report_data.category_overview.to_csv(f'{build_dir}/category_overview.csv', + index=False) + report_data.expenses_by_category.to_csv(f'{build_dir}/expenses_by_category.csv', + index=False) + report_data.total_value.to_csv(f'{build_dir}/total_value.csv', index=False) + report_data.detailed_balance.to_csv(f'{build_dir}/detailed_balance.csv', + index=False) + + +def _compile_document(build_dir): + res_prefix = resources.files(banking_breakdown.document_builder) + + subprocess.call(f"docker build . -f {res_prefix}/Dockerfile.alpine" + " -t banking-breakdown", + shell=True) + subprocess.call("docker run --rm -u `id -u`:`id -g`" + " -v $(realpath .):/project" + f" -w /project/{build_dir} banking-breakdown" + " latexmk -pdf report.tex", + shell=True) + + +def build_document(build_dir: str, report_data: types.ReportData): + _copy_resources(build_dir) + _serialize_report_data(build_dir, report_data) + _compile_document(build_dir) diff --git a/banking_breakdown/document_builder/common.tex b/banking_breakdown/document_builder/common.tex new file mode 100644 index 0000000..92d169f --- /dev/null +++ b/banking_breakdown/document_builder/common.tex @@ -0,0 +1,317 @@ +% Author: Andreas Tsouchlos +% +% Collection of useful commands and definitions +% +% ||====================================================================|| +% || WARNING || +% ||====================================================================|| +% || The following packages have to be included before using this file: || +% || amsmath || +% || pgfplots || +% ||====================================================================|| + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Math Symbols %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +\DeclareMathOperator*{\argmin}{\arg\!\min} +\DeclareMathOperator*{\argmax}{\arg\!\max} +\DeclareMathOperator\sign{sign} + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Data Manipulation %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +% +% Filters for Pgfplots +% Source: https://tex.stackexchange.com/a/58563 (modified) +% + +\pgfplotsset{ + discard if/.style 2 args={ + x filter/.append code={ + \edef\tempa{\thisrow{#1}} + \edef\tempb{#2} + \ifx\tempa\tempb + \def\pgfmathresult{inf} + \fi + } + }, + discard if not/.style 2 args={ + x filter/.append code={ + \edef\tempa{\thisrow{#1}} + \edef\tempb{#2} + \ifx\tempa\tempb + \else + \def\pgfmathresult{inf} + \fi + } + }, + discard if gt/.style 2 args={ + x filter/.append code={ + \edef\tempa{\thisrow{#1}} + \edef\tempb{#2} + \ifdim\tempa pt > \tempb pt + \def\pgfmathresult{inf} + \fi + } + }, + discard if lt/.style 2 args={ + x filter/.append code={ + \edef\tempa{\thisrow{#1}} + \edef\tempb{#2} + \ifdim\tempa pt < \tempb pt + \def\pgfmathresult{inf} + \fi + } + } +} + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Graphics & Plotting %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% +% Colors +% + +% KIT Colors + +\definecolor{kit-green100}{rgb}{0,.59,.51} +\definecolor{kit-green70}{rgb}{.3,.71,.65} +\definecolor{kit-green50}{rgb}{.50,.79,.75} +\definecolor{kit-green30}{rgb}{.69,.87,.85} +\definecolor{kit-green15}{rgb}{.85,.93,.93} +\definecolor{KITgreen}{rgb}{0,.59,.51} + +\definecolor{KITpalegreen}{RGB}{130,190,60} +\colorlet{kit-maigreen100}{KITpalegreen} +\colorlet{kit-maigreen70}{KITpalegreen!70} +\colorlet{kit-maigreen50}{KITpalegreen!50} +\colorlet{kit-maigreen30}{KITpalegreen!30} +\colorlet{kit-maigreen15}{KITpalegreen!15} + +\definecolor{KITblue}{rgb}{.27,.39,.66} +\definecolor{kit-blue100}{rgb}{.27,.39,.67} +\definecolor{kit-blue70}{rgb}{.49,.57,.76} +\definecolor{kit-blue50}{rgb}{.64,.69,.83} +\definecolor{kit-blue30}{rgb}{.78,.82,.9} +\definecolor{kit-blue15}{rgb}{.89,.91,.95} + +\definecolor{KITyellow}{rgb}{.98,.89,0} +\definecolor{kit-yellow100}{cmyk}{0,.05,1,0} +\definecolor{kit-yellow70}{cmyk}{0,.035,.7,0} +\definecolor{kit-yellow50}{cmyk}{0,.025,.5,0} +\definecolor{kit-yellow30}{cmyk}{0,.015,.3,0} +\definecolor{kit-yellow15}{cmyk}{0,.0075,.15,0} + +\definecolor{KITorange}{rgb}{.87,.60,.10} +\definecolor{kit-orange100}{cmyk}{0,.45,1,0} +\definecolor{kit-orange70}{cmyk}{0,.315,.7,0} +\definecolor{kit-orange50}{cmyk}{0,.225,.5,0} +\definecolor{kit-orange30}{cmyk}{0,.135,.3,0} +\definecolor{kit-orange15}{cmyk}{0,.0675,.15,0} + +\definecolor{KITred}{rgb}{.63,.13,.13} +\definecolor{kit-red100}{cmyk}{.25,1,1,0} +\definecolor{kit-red70}{cmyk}{.175,.7,.7,0} +\definecolor{kit-red50}{cmyk}{.125,.5,.5,0} +\definecolor{kit-red30}{cmyk}{.075,.3,.3,0} +\definecolor{kit-red15}{cmyk}{.0375,.15,.15,0} + +\definecolor{KITpurple}{RGB}{160,0,120} +\colorlet{kit-purple100}{KITpurple} +\colorlet{kit-purple70}{KITpurple!70} +\colorlet{kit-purple50}{KITpurple!50} +\colorlet{kit-purple30}{KITpurple!30} +\colorlet{kit-purple15}{KITpurple!15} + +\definecolor{KITcyanblue}{RGB}{80,170,230} +\colorlet{kit-cyanblue100}{KITcyanblue} +\colorlet{kit-cyanblue70}{KITcyanblue!70} +\colorlet{kit-cyanblue50}{KITcyanblue!50} +\colorlet{kit-cyanblue30}{KITcyanblue!30} +\colorlet{kit-cyanblue15}{KITcyanblue!15} + +% Matplotlib Colors + +\definecolor{Mpl1}{HTML}{1f77b4} +\definecolor{Mpl2}{HTML}{ff7f0e} +\definecolor{Mpl3}{HTML}{2ca02c} +\definecolor{Mpl4}{HTML}{d62728} +\definecolor{Mpl5}{HTML}{9467bd} +\definecolor{Mpl6}{HTML}{8c564b} +\definecolor{Mpl7}{HTML}{e377c2} +\definecolor{Mpl8}{HTML}{7f7f7f} +\definecolor{Mpl9}{HTML}{bcbd22} +\definecolor{Mpl10}{HTML}{17becf} + +% +% Color Schemes +% + +% Define colormaps + +\pgfplotsset{ + colormap={mako}{ + rgb=(0.18195582, 0.11955283, 0.23136943) + rgb=(0.25307401, 0.23772973, 0.48316271) + rgb=(0.21607792, 0.39736958, 0.61948028) + rgb=(0.20344718, 0.56074869, 0.65649508) + rgb=(0.25187832, 0.71827158, 0.67872193) + rgb=(0.54578602, 0.8544913, 0.69848331) + }, + colormap={rocket}{ + rgb=(0.20973515, 0.09747934, 0.24238489) + rgb=(0.43860848, 0.12177004, 0.34119475) + rgb=(0.67824099, 0.09192342, 0.3504148) + rgb=(0.8833417, 0.19830556, 0.26014181) + rgb=(0.95381595, 0.46373781, 0.31769923) + rgb=(0.96516917, 0.70776351, 0.5606593) + }, + colormap={cividis}{ + rgb=(0.130669, 0.231458, 0.43284) + rgb=(0.298421, 0.332247, 0.423973) + rgb=(0.42512, 0.431334, 0.447692) + rgb=(0.555393, 0.537807, 0.471147) + rgb=(0.695985, 0.648334, 0.440072) + rgb=(0.849223, 0.771947, 0.359729) + }, + colormap={cel}{ + color=(KITred!90!black); + color=(kit-blue100); + color=(kit-green70); + color=(kit-yellow70!80!kit-orange70); + }, + colormap={matplotlib}{ + % Source: https://github.com/matplotlib/matplotlib/blob/e5a85f960b2d47eac371cff709b830d52c36d267/lib/matplotlib/_cm.py#L1114 + rgb=(0.2298057, 0.298717966, 0.753683153) + rgb=(0.26623388, 0.353094838, 0.801466763) + rgb=(0.30386891, 0.406535296, 0.84495867 ) + rgb=(0.342804478, 0.458757618, 0.883725899) + rgb=(0.38301334, 0.50941904, 0.917387822) + rgb=(0.424369608, 0.558148092, 0.945619588) + rgb=(0.46666708, 0.604562568, 0.968154911) + rgb=(0.509635204, 0.648280772, 0.98478814 ) + rgb=(0.552953156, 0.688929332, 0.995375608) + rgb=(0.596262162, 0.726149107, 0.999836203) + rgb=(0.639176211, 0.759599947, 0.998151185) + rgb=(0.681291281, 0.788964712, 0.990363227) + rgb=(0.722193294, 0.813952739, 0.976574709) + rgb=(0.761464949, 0.834302879, 0.956945269) + rgb=(0.798691636, 0.849786142, 0.931688648) + rgb=(0.833466556, 0.860207984, 0.901068838) + rgb=(0.865395197, 0.86541021, 0.865395561) + rgb=(0.897787179, 0.848937047, 0.820880546) + rgb=(0.924127593, 0.827384882, 0.774508472) + rgb=(0.944468518, 0.800927443, 0.726736146) + rgb=(0.958852946, 0.769767752, 0.678007945) + rgb=(0.96732803, 0.734132809, 0.628751763) + rgb=(0.969954137, 0.694266682, 0.579375448) + rgb=(0.966811177, 0.650421156, 0.530263762) + rgb=(0.958003065, 0.602842431, 0.481775914) + rgb=(0.943660866, 0.551750968, 0.434243684) + rgb=(0.923944917, 0.49730856, 0.387970225) + rgb=(0.89904617, 0.439559467, 0.343229596) + rgb=(0.869186849, 0.378313092, 0.300267182) + rgb=(0.834620542, 0.312874446, 0.259301199) + rgb=(0.795631745, 0.24128379, 0.220525627) + rgb=(0.752534934, 0.157246067, 0.184115123) + rgb=(0.705673158, 0.01555616, 0.150232812) + } +} + +% Define cycle lists + +\pgfplotscreateplotcyclelist{mako}{% + [samples of colormap={4} of mako]% +} +\pgfplotscreateplotcyclelist{rocket}{% + [samples of colormap={4} of rocket]% +} +\pgfplotscreateplotcyclelist{cividis}{% + [samples of colormap={4} of cividis]% +} +\pgfplotscreateplotcyclelist{viridis}{% + [samples of colormap={4} of viridis]% +} +\pgfplotscreateplotcyclelist{cel}{% + [samples of colormap={4} of cel]% +} +\pgfplotscreateplotcyclelist{matplotlib}{% + {Mpl1},{Mpl2},{Mpl3},{Mpl4} +} + +% Define 'scolX' colors + +\makeatletter + +\def\extractcolormapcolor#1#2{% + \expandafter\pgfplotscolormapaccess\expandafter[\pgfplotspointmetatransformedrange]% + [1.0]% + {#2}% + {\pgfkeysvalueof{/pgfplots/colormap name}}% + \def\pgfplots@loc@TMPb{\pgfutil@definecolor{#1}{\csname pgfpl@cm@\pgfkeysvalueof{/pgfplots/colormap name}@colspace\endcsname}}% + \expandafter\pgfplots@loc@TMPb\expandafter{\pgfmathresult}% +}% + +\def\getcolorbyvalue#1{ + \csname pgfpl@cm@\pgfkeysvalueof{/pgfplots/colormap name}@colspace\endcsname +} + +\makeatother + +\def\setschemecolorsfrommap{ + \extractcolormapcolor{scol0}{0} + \extractcolormapcolor{scol1}{333} + \extractcolormapcolor{scol2}{666} + \extractcolormapcolor{scol3}{1000} +} + +\newcommand{\setschemecolorsmanually}[4]{ + \colorlet{scol0}{#1} + \colorlet{scol1}{#2} + \colorlet{scol2}{#3} + \colorlet{scol3}{#4} +} + +% Define color schemes + +\pgfplotsset{ + /pgfplots/colorscheme/cel/.style={ + colormap name={cel}, + cycle list name={cel}, + /utils/exec={\setschemecolorsfrommap}, + }, + /pgfplots/colorscheme/rocket/.style={ + colormap name={rocket}, + cycle list name={rocket}, + /utils/exec={\setschemecolorsfrommap}, + }, + /pgfplots/colorscheme/viridis/.style={ + colormap name={viridis}, + cycle list name={viridis}, + /utils/exec={\setschemecolorsfrommap}, + }, + /pgfplots/colorscheme/mako/.style={ + colormap name={mako}, + cycle list name={mako}, + /utils/exec={\setschemecolorsfrommap}, + }, + /pgfplots/colorscheme/cividis/.style={ + colormap name={cividis}, + cycle list name={cividis}, + /utils/exec={\setschemecolorsfrommap}, + }, + /pgfplots/colorscheme/matplotlib/.style={ + colormap name={matplotlib}, + cycle list name={matplotlib}, + /utils/exec={\setschemecolorsmanually{Mpl1}{Mpl2}{Mpl3}{Mpl4}}, + }, +} + diff --git a/res/report.tex b/banking_breakdown/document_builder/report.tex similarity index 99% rename from res/report.tex rename to banking_breakdown/document_builder/report.tex index 88dd9b7..3abaa24 100644 --- a/res/report.tex +++ b/banking_breakdown/document_builder/report.tex @@ -22,7 +22,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\input{../lib/latex-common/common.tex} +\input{common.tex} \pgfplotsset{colorscheme/rocket} diff --git a/banking_breakdown/test.json b/banking_breakdown/test.json deleted file mode 100644 index bbe1003..0000000 --- a/banking_breakdown/test.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "asdf": [ - "Kinemic" - ] -} \ No newline at end of file diff --git a/banking_breakdown/ui/custom_ui_items.py b/banking_breakdown/ui/custom_ui_items.py index 08dfc11..f2e66e5 100644 --- a/banking_breakdown/ui/custom_ui_items.py +++ b/banking_breakdown/ui/custom_ui_items.py @@ -4,8 +4,11 @@ from PyQt6.QtGui import QPixmap, QAction from PyQt6.QtWidgets import QHBoxLayout, QLabel, QMenu, QInputDialog, \ QMessageBox +import banking_breakdown.ui from banking_breakdown.ui.pandas_model import PandasModel +from importlib import resources + class WarningItem(QHBoxLayout): """Item appearing at top of Window with warning icon.""" @@ -14,7 +17,8 @@ class WarningItem(QHBoxLayout): super(WarningItem, self).__init__() self._warningIcon = QLabel() - pixmap = QPixmap("res/warning.png") + pixmap_path = f"{resources.files(banking_breakdown.ui)}/warning.png" + pixmap = QPixmap(pixmap_path) self._warningIcon.setPixmap(pixmap) self._label = QLabel(text) diff --git a/banking_breakdown/ui/main_window.py b/banking_breakdown/ui/main_window.py index 85a293d..925382b 100644 --- a/banking_breakdown/ui/main_window.py +++ b/banking_breakdown/ui/main_window.py @@ -6,15 +6,19 @@ from PyQt6.QtGui import QAction from PyQt6.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, \ QTableView, QInputDialog, QMessageBox, QFileDialog, QListWidget +import banking_breakdown.ui from banking_breakdown.ui.pandas_model import PandasModel from banking_breakdown.ui.custom_ui_items import WarningItem, HeaderContextMenu +from importlib import resources + class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() - uic.loadUi("res/main_window.ui", self) + ui_path = f"{resources.files(banking_breakdown.ui)}/main_window.ui" + uic.loadUi(ui_path, self) self.setContentsMargins(9, 9, 9, 9) self._warnings = [] diff --git a/res/main_window.ui b/banking_breakdown/ui/main_window.ui similarity index 100% rename from res/main_window.ui rename to banking_breakdown/ui/main_window.ui diff --git a/res/warning.png b/banking_breakdown/ui/warning.png similarity index 100% rename from res/warning.png rename to banking_breakdown/ui/warning.png diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dadc460 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +numpy==1.26.3 +pandas==1.5.3 +PyQt6==6.6.1 +setuptools==69.0.3 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..709ea27 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +import os +from setuptools import setup + + +with open('requirements.txt') as f: + required = f.read().splitlines() + + +setup( + name='banking_breakdown', + version='1.0', + description='Visualize bank statements', + author='Andreas Tsouchlos', + packages=['banking_breakdown', 'banking_breakdown.ui', 'banking_breakdown.document_builder'], + install_requires=required, + package_data={'banking_breakdown.ui': ['main_window.ui', 'warning.png'], + 'banking_breakdown.document_builder': ['.latexmkrc', 'Dockerfile.alpine', 'report.tex', 'common.tex']}, + include_package_data=True, +) +