From e6eeaa6d863dfcef8bf60715e418b875a3dcc800 Mon Sep 17 00:00:00 2001 From: Christian Heller <c.heller@plomlompom.de> Date: Sat, 30 Sep 2023 01:10:23 +0200 Subject: [PATCH] Add food tracker. --- calories.py | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 calories.py diff --git a/calories.py b/calories.py new file mode 100644 index 0000000..771b11a --- /dev/null +++ b/calories.py @@ -0,0 +1,293 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import os +import json + +def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows): + return """<html> +<meta charset="UTF-8"> +<style> +table { + margin-bottom: 2em; +} +td, th { + text-align: right; +} +</style>""" + f""" +<body> +<form action="/" method="POST"> +<td><input name="update" type="submit" value="update" /></td> +<table> +<tr><th>title</th><th>calories</th><th>sugar (g)</th><th>standard weight (g)</th><th>comments</th><th>delete</th></tr> +{eatable_rows} +<tr> +<th>add:</th> +</tr> +<tr> +<td><input name="title" type="text" value="" /></td> +<td><input name="cals" type="number" min="0" step="0.1" value="0" /></td> +<td><input name="sugar_g" type="number" min="0" step="0.1" value="0" /></td> +<td><input name="standard_g" type="number" min="1" step="0.1" value="1" /></td> +<td><input name="comments" type="text" value="" /></td> +</tr> +</table> +<table> +<tr><th>day</th><th>calories</th><th>sugar (g)</th></tr> +{day_rows} +</table> +<table> +<tr><th>eatable</th><th>unit count</th><th>unit weight (g)</th><th>calories</th><th>sugar (g)</th></tr> +{consumption_rows} +<tr> +<th>add from DB:</th> +</tr> +<tr> +<td><select name="eatable_key">{eatables_selection}</select></td> +<td><input name="unit_count" type="number" step="1" min="0" value="0" /></td> +<td></td> +</tr> +</table> +</form> +</body> +</html>""" + +class LockFileDetected(Exception): + pass + +class Eatable: + + def __init__(self, title, cals, sugar_g, standard_g=100, comments=""): + self.title = title + self.cals = cals # per 100g + self.sugar_g = sugar_g # per 100g + self.standard_g = standard_g # common unit weight + self.comments = comments + + def to_dict(self): + return { + "title": self.title, + "cals": self.cals, + "sugar_g": self.sugar_g, + "standard_g": self.standard_g, + "comments": self.comments + } + +class Consumption: + + def __init__(self, eatable_key, unit_count=None): + self.eatable_key = eatable_key + self.unit_count = unit_count + + def to_dict(self): + return { + "eatable_key": self.eatable_key, + "unit_count": self.unit_count, + } + +class Day: + + def __init__(self, calories, sugar_g): + self.calories = calories + self.sugar_g = sugar_g + + def to_dict(self): + return { + "calories": self.calories, + "sugar_g": self.sugar_g, + } + +class Database: + + def __init__(self, load_from_file=True): + db_name = "calories_db" + self.db_file = db_name + ".json" + self.lock_file = db_name+ ".lock" + 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)) + + def from_dict(self, d): + self.set_today_date(d["today_date"]) + for k,v in d["eatables"].items(): + self.add_eatable(k, Eatable(v["title"], v["cals"], v["sugar_g"], v["standard_g"], v["comments"])) + for c in d["consumptions"]: + self.add_consumption(Consumption(c["eatable_key"], c["unit_count"])) + for k,v in d["days"].items(): + self.add_day(k, Day(v["calories"], v["sugar_g"])) + + def to_dict(self): + d = {"eatables": {}, "consumptions": [], "days":{}, "today_date":self.today_date} + for k,v in self.eatables.items(): + d["eatables"][k] = v.to_dict() + for c in self.consumptions: + d["consumptions"] += [c.to_dict()] + for k,v in self.days.items(): + d["days"][k] = v.to_dict() + return d + + def calc_consumption(self, c): + eatable = self.eatables[c.eatable_key] + calories = eatable.cals * c.unit_count + sugar_g = eatable.sugar_g * c.unit_count + # calories = float(eatable.cals / eatable.standard_g) * c.unit_count * c.unit_weight + # sugar_g = float(eatable.sugar_g / eatable.standard_g) * c.unit_count * c.unit_weight + self.today.calories += calories + self.today.sugar_g += sugar_g + return {"cals": calories, "sugar": sugar_g } + + def eatables_selection(self, selection=None): + html = '' # if selection else '<option value="" />' + for k,v in self.eatables.items(): + selected = ' selected' if k==selection else '' + html += '<option value="%s"%s>%s</option>' % (k, selected, v.title) + return html + + def add_eatable(self, id_, eatable): + self.eatables[id_] = eatable + + def add_consumption(self, consumption): + self.consumptions += [consumption] + + def add_day(self, date, day): + self.days[date] = day + + def set_today_date(self, today_date): + self.today_date = today_date + + def delete(self, id_): + 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) + + +class MyServer(BaseHTTPRequestHandler): + + def do_POST(self): + from uuid import uuid4 + from urllib.parse import parse_qs + import datetime + length = int(self.headers['content-length']) + postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + db = Database(False) + def decode(key, i, is_num=True): + if is_num: + return float(postvars[key][i]) + return postvars[key][i] + to_delete = [] + if 'delete' in postvars.keys(): + for target in postvars['delete']: + to_delete += [target] + i = 0 + if 'eatable_uuid' in postvars.keys(): + for uuid_encoded in postvars['eatable_uuid']: + uuid = uuid_encoded + if uuid not in to_delete: + e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False)) + db.add_eatable(uuid, e) + i += 1 + if 'title' in postvars.keys() and len(postvars['title'][i]) > 0: + e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False)) + db.add_eatable(str(uuid4()), e) + i = 0 + if 'eatable_key' in postvars.keys(): + for eatable_key in postvars['eatable_key']: + c = Consumption(decode("eatable_key", i, False), decode("unit_count", i)) + i += 1 + if c.unit_count == 0: + continue + db.add_consumption(c) + i = 0 + if 'day_date' in postvars.keys(): + for date in postvars['day_date']: + db.add_day((date), Day(decode("day_cals", i), decode("day_sugar", i))) + i += 1 + if 'new_date' in postvars.keys(): + db.set_today_date(postvars["new_date"][0]) + if 'archive_day' in postvars.keys(): + new_cals = postvars["new_day_cals"][0] + new_sugar = postvars["new_day_sugar"][0] + db.add_day(db.today_date, Day(float(new_cals), float(new_sugar))) + db.set_today_date(str(datetime.datetime.now()))#[:10]) + db.consumptions = [] + 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")) + + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + db = Database() + + eatables = "" + for k,v in db.eatables.items(): + eatables += "<tr>"\ + "<input name=\"eatable_uuid\" type=\"hidden\" value=\"%s\" />"\ + "<td><input name=\"title\" value=\"%s\" /></td>"\ + "<td><input name=\"cals\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\ + "<td><input name=\"sugar_g\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\ + "<td><input name=\"standard_g\" type=\"number\" step=\"0.1\" min=\"1\" value=\"%1.f\" /></td>"\ + "<td><input name=\"comments\" value=\"%s\" /></td>"\ + "<td><input name=\"delete\" type=\"checkbox\" value=\"%s\" /></td>"\ + "</tr>" % (k, v.title, v.cals, v.sugar_g, v.standard_g, v.comments, k) + consumptions = "" + for c in db.consumptions: + r = db.calc_consumption(c) + consumptions += "<tr />"\ + "<td><select name=\"eatable_key\">%s</select></td>"\ + "<td><input name=\"unit_count\" type=\"number\" min=\"0\" value=\"%d\" /></td>"\ + "<td></td>"\ + "<td>%.1f</td>"\ + "<td>%.1f</td>"\ + "</tr>" % (db.eatables_selection(c.eatable_key), c.unit_count, r["cals"], r["sugar"]) + day_rows = "" + for date, day in db.days.items(): + day_rows += "<tr>"\ + "<td><input name=\"day_date\" type=\"hidden\" value=\"%s\" />%s</td>"\ + "<td><input name=\"day_cals\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\ + "<td><input name=\"day_sugar\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\ + "</tr>" % (date, date[:10], day.calories, day.calories, day.sugar_g, day.sugar_g) + day_rows += "<tr>"\ + "<th>today:</th><th></th><th></th><th>archive?</th>"\ + "</tr>"\ + "<tr>"\ + "<td><input name=\"new_date\" size=8 value=\"%s\" /></td>"\ + "<td><input name=\"new_day_cals\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\ + "<td><input name=\"new_day_sugar\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\ + "<td><input name=\"archive_day\" type=\"checkbox\" /></td>"\ + "</tr>" % (db.today_date, db.today.calories, db.today.calories, db.today.sugar_g, db.today.sugar_g) + page = build_page(eatables, consumptions, db.eatables_selection(), day_rows) + self.wfile.write(bytes(page, "utf-8")) + + +hostName = "localhost" +serverPort = 8080 +if __name__ == "__main__": + webServer = HTTPServer((hostName, serverPort), MyServer) + print("Server started http://%s:%s" % (hostName, serverPort)) + try: + webServer.serve_forever() + except KeyboardInterrupt: + pass + webServer.server_close() + print("Server stopped.") -- 2.30.2