From 436e0f40b45319ef4452ce6ced1a3c3df813119b Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 19 Nov 2023 06:12:13 +0100 Subject: [PATCH] Refactor all accounting scripts. --- calories.py | 199 ++++++++++++++++++++------------------- income_progress_bars.py | 37 ++++---- ledger.py | 201 ++++++++++++++++++++-------------------- plomlib.py | 39 ++++---- 4 files changed, 241 insertions(+), 235 deletions(-) diff --git a/calories.py b/calories.py index 5d2fc98..88165b8 100644 --- a/calories.py +++ b/calories.py @@ -1,43 +1,76 @@ -from http.server import BaseHTTPRequestHandler, HTTPServer import os import json import datetime +import jinja2 +from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer -hostName = "localhost" -serverPort = 8081 +db_path = '/home/plom/org/calories_db.json' -def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows): - return """ - +server_port = 8081 + +tmpl = """ """ + f""" +
-{consumption_rows} +{% for c in consumptions %} + + + + + + + + +{% endfor %} - +
eatableunit countunit weight (g)caloriessugar (g)
{{c.title}}{{c.cals}}{{c.sugar}}
add from DB:
+ + + + + -{day_rows} +{% for d in days %} + + + + + +{% endfor %}
today:archive?
+{{db.today.calories}}{{db.today.sugar_g}}
daycaloriessugar (g)
{{d.date_short|e}}{{d.cals}}{{d.sugar}}
-{eatable_rows} +{% for e in eatables %} + + + + + + + +{% endfor %} @@ -52,7 +85,6 @@ input[type="number"] { text-align: right; } - """ -class LockFileDetected(Exception): - pass class Eatable: @@ -92,6 +121,7 @@ class Eatable: "popularity": self.popularity } + class Consumption: def __init__(self, eatable_key, unit_count=None, keep_visible=0): @@ -106,6 +136,7 @@ class Consumption: "keep_visible": self.keep_visible } + class Day: def __init__(self, calories, sugar_g): @@ -118,20 +149,22 @@ class Day: "sugar_g": self.sugar_g, } -class Database: + +class CaloriesDB(PlomDB): def __init__(self, load_from_file=True): - db_name = "calories_db" - self.db_file = db_name + ".json" - self.lock_file = db_name+ ".lock" + self.load_from_file = load_from_file self.eatables = {} self.consumptions = [] self.days = {} self.today = Day(0, 0) self.today_date = "" - if load_from_file and os.path.exists(self.db_file): - with open(self.db_file, "r") as f: - self.from_dict(json.load(f)) + super().__init__(db_path) + + def read_db_file(self, f): + if not self.load_from_file: + return + self.from_dict(json.load(f)) def from_dict(self, d): self.set_today_date(d["today_date"]) @@ -161,14 +194,14 @@ class Database: return {"cals": calories, "sugar": sugar_g } def eatables_selection(self): - html = '' + options = [] already_selected = [c.eatable_key for c in self.consumptions] for k, v in sorted(self.eatables.items(), key=lambda item: item[1].title): if k in already_selected: continue v = self.eatables[k] - html += '' % (k, v.title) - return html + options += [(k, v.title)] + return options def add_eatable(self, id_, eatable): self.eatables[id_] = eatable @@ -188,26 +221,17 @@ class Database: del self.eatables[id_] def write(self): - import shutil - if os.path.exists(self.lock_file): - raise LockFileDetected - if os.path.exists(self.db_file): - shutil.copy(self.db_file, self.db_file + ".bak") - f = open(self.lock_file, "w+") - f.close() - with open(self.db_file, "w") as f: - json.dump(self.to_dict(), f) - os.remove(self.lock_file) + self.write_text_to_db(json.dumps(self.to_dict())) -class MyServer(BaseHTTPRequestHandler): +class CaloriesServer(PlomServer): def do_POST(self): from uuid import uuid4 from urllib.parse import parse_qs length = int(self.headers['content-length']) postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - db = Database(False) + db = CaloriesDB(False) def decode(key, i, is_num=True): if is_num: return float(postvars[key][i]) @@ -259,70 +283,51 @@ class MyServer(BaseHTTPRequestHandler): break try: db.write() - self.send_response(302) - self.send_header('Location', '/') - self.end_headers() - except LockFileDetected: - self.send_response(400) - self.end_headers() - self.wfile.write(bytes("Sorry, lock file!", "utf-8")) + self.redirect() + except PlomException as e: + self.fail_400(e) def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - db = Database() - - eatables = "" + db = CaloriesDB() + # eatables = "" + eatable_rows = [] for k,v in db.eatables.items(): - eatables += ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - "" % (k, v.title, v.cals, v.sugar_g, v.standard_g, v.comments, k) - consumptions = "" + eatable_rows += [{ + 'uuid': k, + 'title': v.title, + 'cals': f'{v.cals:.1f}', + 'sugar_g': f'{v.sugar_g:.1f}', + 'standard_g': f'{v.standard_g:.1f}', + 'comments': v.comments + }] db.consumptions = sorted(db.consumptions, key=lambda x: db.eatables[x.eatable_key].title) + consumption_rows = [] for c in db.consumptions: r = db.calc_consumption(c) - consumptions += ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - "" % (c.eatable_key, c.unit_count, db.eatables[c.eatable_key].title, r["cals"], r["sugar"]) - day_rows = "" - for date in sorted(db.days.keys()): + consumption_rows += [{ + 'key': c.eatable_key, + 'count': c.unit_count, + 'title': db.eatables[c.eatable_key].title, + 'cals': r['cals'], + 'sugar': r['sugar'] + }] + day_rows = [] + for date in reversed(sorted(db.days.keys())): day = db.days[date] - day_rows = ""\ - ""\ - ""\ - ""\ - "" % (date, date[:10], day.calories, day.calories, day.sugar_g, day.sugar_g) + day_rows - day_rows = ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - ""\ - "" % (db.today_date, db.today.calories, db.today.calories, db.today.sugar_g, db.today.sugar_g) + day_rows - page = build_page(eatables, consumptions, db.eatables_selection(), day_rows) - self.wfile.write(bytes(page, "utf-8")) - - -if __name__ == "__main__": - webServer = HTTPServer((hostName, serverPort), MyServer) - print(f"Server started http://{hostName}:{serverPort}") - try: - webServer.serve_forever() - except KeyboardInterrupt: - pass - webServer.server_close() - print("Server stopped.") + day_rows += [{ + 'date': date, + 'date_short': date[:10], + 'cals': f'{day.calories:.1f}', + 'sugar': f'{day.sugar_g:.1f}', + }] + page = jinja2.Template(tmpl).render( + db=db, + days=day_rows, + consumptions=consumption_rows, + eatables=eatable_rows, + eatables_selection=db.eatables_selection()) + self.send_HTML(page) + + +if __name__ == "__main__": + run_server(server_port, CaloriesServer) diff --git a/income_progress_bars.py b/income_progress_bars.py index 94a3456..553627d 100644 --- a/income_progress_bars.py +++ b/income_progress_bars.py @@ -1,13 +1,12 @@ -# from http.server import BaseHTTPRequestHandler, HTTPServer import os import json import jinja2 from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer -server_port = 8081 +server_port = 8083 +db_path = '/home/plom/org/income.json' -tmpl = jinja2.Template(""" - +tmpl = """
titlecaloriessugar (g)standard weight (g)commentsdelete
+ +
add:
%s%.1f%.1f
%s%.1f%.1f
today:archive?
%.1f%.1f
- -{% for p in progress_bars %} - + +{% for p in progress_bars %} - - -{% endfor %} -
earnedprogresssurplus
{{p.title}}
earnedprogresssurplus
{{p.title}} {{p.earned|round(2)}}{% if p.time_progress >= 0 %}
{% endif %}
{{p.diff_goal}}
+{% if p.time_progress >= 0 %}
{% endif %}
+
{{p.diff_goal}}
+{% endfor %} +
+
hourly rateworked today
€ minutes
€ minutes
€ minutes
@@ -103,8 +102,8 @@ table { -""") +""" + class IncomeDB(PlomDB): @@ -124,7 +123,7 @@ class IncomeDB(PlomDB): self.workday_minutes_worked_3 = 0, self.year_goal = 20000, self.workdays_per_month = 16 - super().__init__('_income') + super().__init__(db_path) def read_db_file(self, f): d = json.load(f) @@ -143,6 +142,7 @@ class IncomeDB(PlomDB): def write_db(self): self.write_text_to_db(json.dumps(self.to_dict())) + class ProgressBar: def __init__(self, title, earned, goal, time_progress=-1): self.title = title @@ -160,7 +160,7 @@ class ProgressBar: if time_progress > 0: self.success = success_income / time_progress -# class MyServer(BaseHTTPRequestHandler): + class IncomeServer(PlomServer): def do_POST(self): @@ -188,9 +188,7 @@ class IncomeServer(PlomServer): db.workday_minutes_worked_2 = 0 db.workday_minutes_worked_3 = 0 db.write_db() - self.send_response(302) - self.send_header('Location', '/') - self.end_headers() + self.redirect() except PlomException as e: self.fail_400(e) @@ -237,7 +235,7 @@ class IncomeServer(PlomServer): ProgressBar("month", month_plus, month_goal, progress_time_month), ProgressBar("week", week_plus, week_goal, progress_time_week), ProgressBar("workday", day_income, workday_goal)] - page = tmpl.render( + page = jinja2.Template(tmpl).render( progress_bars = progress_bars, workday_hourly_rate_1 = db.workday_hourly_rate_1, workday_minutes_worked_1 = db.workday_minutes_worked_1, @@ -256,5 +254,6 @@ class IncomeServer(PlomServer): except PlomException as e: self.fail_400(e) + if __name__ == "__main__": run_server(server_port, IncomeServer) diff --git a/ledger.py b/ledger.py index b4d9fd9..244a684 100755 --- a/ledger.py +++ b/ledger.py @@ -1,6 +1,4 @@ -from http.server import BaseHTTPRequestHandler, HTTPServer import os -import html import jinja2 import decimal from datetime import datetime, timedelta @@ -8,6 +6,89 @@ from urllib.parse import parse_qs, urlparse from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer server_port = 8082 +db_path = '/home/plom/org/ledger2023.dat' + +html_head = """ + + +ledger +balance +add free +add structured +
+""" +booking_html = """ +

{{date}} {{desc}} {{head_comment|e}}
+[edit: structured +/ free +| copy:structured +/ free +| move {% if move_up %}up{% else %}up{% endif %}/{% if move_down %}down{% else %}down{% endif %} +| balance after +] +

yearly income goal€
monthly income goal{{month_goal|round(2)}}€
+{% for l in booking_lines %} +{% if l.acc %} + +{% else %} + +{% endif %} +{% endfor %} +
{{l.acc|e}}{{l.money|e}}{{l.comment|e}}
{{l.comment|e}}

+""" +add_form_header = """
+ + +""" +add_form_footer = """ + + + +
+""" +add_free_html = """
+ +""" +add_structured_html = """ + + + +
+ + + + +
+{% for line in booking_lines %} + + + + + + + +
+{% endfor %} +{% for name, items in datalist_sets.items() %} + +{% for item in items %} + +{% endfor %} + +{% endfor %} +""" def apply_booking_to_account_balances(account_sums, account, currency, amount): @@ -256,7 +337,7 @@ class LedgerDB(PlomDB): self.bookings = [] self.comments = [] self.real_lines = [] - super().__init__('_ledger') + super().__init__(db_path) ret = parse_lines(self.real_lines) self.bookings += ret[0] self.comments += ret[1] @@ -433,55 +514,9 @@ class LedgerDB(PlomDB): class LedgerServer(PlomServer): - header = """ - - - -ledger -balance -add free -add structured -
-""" - booking_tmpl = jinja2.Template(""" -

{{date}} {{desc}} {{head_comment|e}}
-[edit: structured -/ free -| copy:structured -/ free -| move {% if move_up %}up{% else %}up{% endif %}/{% if move_down %}down{% else %}down{% endif %} -| balance after -] - -{% for l in booking_lines %} -{% if l.acc %} - -{% else %} - -{% endif %} -{% endfor %} -
{{l.acc|e}}{{l.money|e}}{{l.comment|e}}
{{l.comment|e}}

-""") - add_form_header = """
- - -""" - add_form_footer = """ - - - -
-""" - footer = "\n" + + def pre_init(self): + self.html_head += [html_head] def do_POST(self): try: @@ -522,15 +557,14 @@ input[type=number] { text-align: right; font-family: monospace; } nth = db.get_nth_for_booking_of_start_line(new_start) if new_start > start: nth -= 1 - redir_url = f'/#{nth}' - self.send_code_and_headers(302, [('Location', redir_url)]) + self.redirect( f'/#{nth}') # otherwise just re-build editing form else: if '/add_structured' == parsed_url.path: edit_content = self.add_structured(db, start, end, temp_lines=lines, add_empty_line=add_empty_line) else: edit_content = self.add_free(db, start, end) - self.send_HTML(self.header + edit_content + self.footer) + self.send_HTML(edit_content) except PlomException as e: self.fail_400(e) @@ -541,29 +575,27 @@ input[type=number] { text-align: right; font-family: monospace; } start = int(params.get('start', ['0'])[0]) end = int(params.get('end', ['0'])[0]) db = LedgerDB() - page = self.header if parsed_url.path == '/balance': stop = params.get('stop', [None])[0] - page += self.balance_as_html(db, stop) + page = self.balance_as_html(db, stop) elif parsed_url.path == '/add_free': - page += self.add_free(db, start, end) + page = self.add_free(db, start, end) elif parsed_url.path == '/add_structured': - page += self.add_structured(db, start, end) + page = self.add_structured(db, start, end) elif parsed_url.path == '/copy_free': - page += self.add_free(db, start, end, copy=True) + page = self.add_free(db, start, end, copy=True) elif parsed_url.path == '/copy_structured': - page += self.add_structured(db, start, end, copy=True) + page = self.add_structured(db, start, end, copy=True) elif parsed_url.path == '/move_up': nth = self.move_up(db, start, end) - self.send_code_and_headers(302, [('Location', f'/#{nth}')]) + self.redirect(f'/#{nth}') return elif parsed_url.path == '/move_down': nth = self.move_down(db, start, end) - self.send_code_and_headers(302, [('Location', f'/#{nth}')]) + self.redirect(f'/#{nth}') return else: - page += self.ledger_as_html(db) - page += self.footer + page = self.ledger_as_html(db) self.send_HTML(page) except PlomException as e: self.fail_400(e) @@ -644,6 +676,7 @@ input[type=number] { text-align: right; font-family: monospace; } return f"
{content}
" def ledger_as_html(self, db): + booking_tmpl = jinja2.Template(booking_html) single_c_tmpl = jinja2.Template('{{c|e}}
') ## elements_to_write = [] last_i = i = 0 ## @@ -665,7 +698,7 @@ input[type=number] { text-align: right; font-family: monospace; } if booking_line[1] is not None: money = f'{booking_line[1]} {booking_line[2]}' booking_lines += [{'acc': booking_line[0], 'money':money, 'comment':comment}] ## - elements_to_write += [self.booking_tmpl.render( + elements_to_write += [booking_tmpl.render( nth=nth, start=booking.start_line, end=booking_end, @@ -679,46 +712,14 @@ input[type=number] { text-align: right; font-family: monospace; } return '\n'.join(elements_to_write) def add_free(self, db, start=0, end=0, copy=False): - tmpl = jinja2.Template(self.add_form_header + """
- -""" + self.add_form_footer) + tmpl = jinja2.Template(add_form_header + add_free_html + add_form_footer) lines = db.get_lines(start, end) if copy: start = end = 0 return tmpl.render(action='add_free', start=start, end=end, lines=lines) def add_structured(self, db, start=0, end=0, copy=False, temp_lines=[], add_empty_line=None): - tmpl = jinja2.Template(self.add_form_header + """ - - - -
- - - - -
-{% for line in booking_lines %} - - - - - - - -
-{% endfor %} -{% for name, items in datalist_sets.items() %} - -{% for item in items %} - -{% endfor %} - -{% endfor %} -""" + self.add_form_footer) + tmpl = jinja2.Template(add_form_header + add_structured_html + add_form_footer) lines = temp_lines if len(''.join(temp_lines)) > 0 else db.get_lines(start, end) bookings, comments = parse_lines(lines, validate_bookings=False) if len(bookings) > 1: diff --git a/plomlib.py b/plomlib.py index 9dac712..92bfb85 100644 --- a/plomlib.py +++ b/plomlib.py @@ -9,10 +9,10 @@ class PlomException(Exception): class PlomDB: def __init__(self, db_name): - self.db_file = db_name + ".json" - self.lock_file = db_name+ ".lock" + self.db_file = db_name + self.lock_file = db_name+ '.lock' if os.path.exists(self.db_file): - with open(self.db_file, "r") as f: + with open(self.db_file, 'r') as f: self.read_db_file(f) def lock(self): @@ -78,27 +78,24 @@ class PlomDB: class PlomServer(BaseHTTPRequestHandler): - header = '' - footer = '' - - def run(self, port): - from http.server import HTTPServer - webServer = HTTPServer(('localhost', port), type(self)) - print(f"Server started http://localhost:{port}") - try: - webServer.serve_forever() - except KeyboardInterrupt: - pass - webServer.server_close() - print("Server stopped.") + + def __init__(self, *args, **kwargs): + self.html_head = ['\n\n'] + self.html_foot = ['\n'] + self.pre_init() + super().__init__(*args, **kwargs) + + def pre_init(self): + pass def fail_400(self, e): - page = f'{self.header}ERROR: {e}{self.footer}' - self.send_HTML(page, 400) + self.send_HTML(f'ERROR: {e}', 400) def send_HTML(self, html, code=200): self.send_code_and_headers(code, [('Content-type', 'text/html')]) - self.wfile.write(bytes(html, "utf-8")) + header = '\n'.join(self.html_head) + footer = '\n'.join(self.html_foot) + self.wfile.write(bytes(f'{header}\n{html}\n{footer}', 'utf-8')) def send_code_and_headers(self, code, headers=[]): self.send_response(code) @@ -106,6 +103,10 @@ class PlomServer(BaseHTTPRequestHandler): self.send_header(fieldname, content) self.end_headers() + def redirect(self, url='/'): + self.send_code_and_headers(302, [('Location', url)]) + + def run_server(port, server_class): from http.server import HTTPServer -- 2.30.2