diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cfc6b6..7cf9560 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: build on: push: + branches: + - main tags: - 'v*.*.*' jobs: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9de222f..c8cf7ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,21 @@ Changelog `_, and this project adheres to `Semantic Versioning `_. +[v0.5.9 unreleased] +------------------- + +In Progress +====== + +- a database interface + +[v0.5.8] +-------- + +Hotfix +====== + +- plot figure saving fixed [v0.5.7] -------- diff --git a/db/create.py b/db/create.py new file mode 100644 index 0000000..8b1e0f0 --- /dev/null +++ b/db/create.py @@ -0,0 +1,92 @@ +import sqlite3 +from sqlite3 import Error + +PATH = r".\db\example.db" + +# flake8: noqa + + +def create_connection(): + """create a database connection to a database that resides + in the memory + """ + conn = None + try: + conn = sqlite3.connect(PATH) + except Error as e: + print(e) + finally: + return conn + + +def create_table(conn, create_table_sql): + """create a table from the create_table_sql statement + :param conn: Connection object + :param create_table_sql: a CREATE TABLE statement + :return: + """ + try: + c = conn.cursor() + c.execute(create_table_sql) + except Error as e: + print(e) + + +def main(): + # trailing commas will throw a syntax error !? + + sql_create_projects_table = """ + CREATE TABLE IF NOT EXISTS projects ( + id integer PRIMARY KEY, + name text NOT NULL + ); + """ + + sql_create_tests_table = """ + CREATE TABLE IF NOT EXISTS tests ( + id integer PRIMARY KEY, + name text NOT NULL, + project_id integer NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects (id) + ); + """ + + sql_create_readings_table = """ + CREATE TABLE IF NOT EXISTS readings ( + id integer PRIMARY KEY, + name text NOT NULL, + pump_1 integer NOT NULL, + pump_2 integer NOT NULL, + test_id integer NOT NULL, + FOREIGN KEY (test_id) REFERENCES tests (id) + ); + """ + + sql_create_groups_table = """ + CREATE TABLE IF NOT EXISTS groups ( + id integer NOT NULL, + listID NOT NULL, + name text NOT NULL, + ) + """ + + # create a database connection + conn = create_connection() + # create tables + if conn is not None: + # create projects table + print("projects") + create_table(conn, sql_create_projects_table) + + print("tasks") + # create tasks table + create_table(conn, sql_create_tests_table) + else: + print("Error! cannot create the database connection.") + + conn.commit() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/db/sampledb.json b/db/sampledb.json new file mode 100644 index 0000000..1a43b92 --- /dev/null +++ b/db/sampledb.json @@ -0,0 +1,42 @@ +{ + "_default": { + "1": { + "uuid":"uuid", + "name": "project", + "customer": "customer", + "productionCo": "productionCo", + "submittedBy": "submittedBy", + "field": "field", + "sample": "sample", + "sampleDate": "sampleDate", + "recDate": "recDate", + "compDate": "compDate", + "analyst": "analyst", + "numbers": "numbers", + "notes": "notes", + "tests": [ + { + "name": "testname", + "reportAs":"label", + "isBlank": true, + "chemical": "chemical", + "rate": 100, + "clarity": "clarity", + "toConsider": "toConsider", + "includeOnRep": "includeOnRep", + "obsBaseline": 75, + "notes": "notes", + "result": 100, + "readings":[ + { + "average": 2, + "pump 1": 1, + "pump 2": 3, + "elapsedMin": "stamp" + } + ] + } + ] + } + } +} diff --git a/db/sampledb.yaml b/db/sampledb.yaml new file mode 100644 index 0000000..dbea809 --- /dev/null +++ b/db/sampledb.yaml @@ -0,0 +1,33 @@ +--- +_default: + '1': + uuid: uuid + name: project + customer: customer + productionCo: productionCo + submittedBy: submittedBy + field: field + sample: sample + sampleDate: sampleDate + recDate: recDate + compDate: compDate + analyst: analyst + numbers: numbers + notes: notes + tests: + - name: testname + reportAs: label + isBlank: true + chemical: chemical + rate: 100 + clarity: clarity + toConsider: toConsider + includeOnRep: includeOnRep + obsBaseline: 75 + notes: notes + result: 100 + readings: + - average: 2 + pump 1: 1 + pump 2: 3 + elapsedMin: stamp diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..a4ae05e --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,44 @@ +-- +-- File generated with SQLiteStudio v3.3.3 on Wed Jun 30 07:22:45 2021 +-- +-- Text encoding used: System +-- +PRAGMA foreign_keys = off; +BEGIN TRANSACTION; + +-- Table: projects +CREATE TABLE projects ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + + +-- Table: readings +CREATE TABLE readings ( + id INTEGER PRIMARY KEY, + test_id INTEGER REFERENCES tests (id) ON DELETE CASCADE + ON UPDATE CASCADE + NOT NULL +); + + +-- Table: reports +CREATE TABLE reports ( + id INTEGER PRIMARY KEY, + test_id REFERENCES tests (id) ON DELETE CASCADE + ON UPDATE CASCADE +); + + +-- Table: tests +CREATE TABLE tests ( + id INTEGER PRIMARY KEY, + project_id REFERENCES projects (id) ON DELETE CASCADE + ON UPDATE CASCADE + NOT NULL, + name TEXT NOT NULL +); + + +COMMIT TRANSACTION; +PRAGMA foreign_keys = on; diff --git a/poetry.lock b/poetry.lock index 3ebdd03..1284de4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -276,6 +276,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tinydb" +version = "4.4.0" +description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + [[package]] name = "tkcalendar" version = "1.6.1" @@ -332,7 +340,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "c8aea8aaf25674e2103327faab3031b697a230a0b36c8015cd08cbbb87a79d4e" +content-hash = "3108fb9291bfae1f579f9170ddec544c2c3e314247ace01876975a82c8941338" [metadata.files] appdirs = [ @@ -583,6 +591,10 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +tinydb = [ + {file = "tinydb-4.4.0-py3-none-any.whl", hash = "sha256:30b0f718ebb288e42d2f69f3e1b18928739f25153e6b5308a234e95c1673de71"}, + {file = "tinydb-4.4.0.tar.gz", hash = "sha256:d57c29524ecacc081ebc24f96e0d787bba11dc20d52634a32a709b878be3545a"}, +] tkcalendar = [ {file = "tkcalendar-1.6.1-py3-none-any.whl", hash = "sha256:9d3a80816a7b32d64fab696fa3d2a007fb23c87953267d5e343a38ff4cd7c15c"}, {file = "tkcalendar-1.6.1-py3.8.egg", hash = "sha256:c3ac34ab268734377ce73407893e8a5765e288aecbbb55136fb3ccea98006a96"}, diff --git a/pyproject.toml b/pyproject.toml index 99f6089..6632ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "scalewiz" -version = "0.5.7" +version = "0.5.8" description = "A graphical user interface for chemical performance testing designed to work with Teledyne SSI MX-class HPLC pumps." readme = "README.rst" -license = "GPL-3.0-or-later" +license = "GPL-3.0" authors = ["Alex Whittington "] +repository = "https://github.com/teauxfu/scalewiz" packages = [ {include = "scalewiz"} ] @@ -29,6 +30,7 @@ pandas = "^1.2.2" py-hplc = "^1.0.1" tomlkit = "^0.7.0" appdirs = "^1.4.4" +tinydb = "^4.4.0" [tool.poetry.scripts] scalewiz = "scalewiz.__main__:main" diff --git a/requirements.txt b/requirements.txt index 13c0ae5..c839dca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -154,6 +154,9 @@ pytz==2021.1; python_full_version >= "3.7.1" \ six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 +tinydb==4.4.0; python_version >= "3.5" and python_version < "4.0" \ + --hash=sha256:30b0f718ebb288e42d2f69f3e1b18928739f25153e6b5308a234e95c1673de71 \ + --hash=sha256:d57c29524ecacc081ebc24f96e0d787bba11dc20d52634a32a709b878be3545a tkcalendar==1.6.1 \ --hash=sha256:9d3a80816a7b32d64fab696fa3d2a007fb23c87953267d5e343a38ff4cd7c15c \ --hash=sha256:c3ac34ab268734377ce73407893e8a5765e288aecbbb55136fb3ccea98006a96 \ diff --git a/scalewiz/__init__.py b/scalewiz/__init__.py index e8cdd3e..87d3aca 100644 --- a/scalewiz/__init__.py +++ b/scalewiz/__init__.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING +from tinydb import TinyDB + if TYPE_CHECKING: from tkinter import Tk @@ -11,3 +13,4 @@ ROOT: Tk = None CONFIG: dict = get_config() +DATABASE: TinyDB = TinyDB(CONFIG["recents"]["database"]) diff --git a/scalewiz/components/evaluation_window.py b/scalewiz/components/evaluation_window.py index 02ad3a0..17142ae 100644 --- a/scalewiz/components/evaluation_window.py +++ b/scalewiz/components/evaluation_window.py @@ -119,7 +119,7 @@ def save(self) -> None: ) parent_dir = Path(self.editor_project.path.get()).parent plot_output = Path(parent_dir, plot_output).resolve() - self.plot_view.fig.savefig(plot_output) + self.plot_view.fig.savefig(str(plot_output)) self.editor_project.plot.set(str(plot_output)) # update log log_output = ( diff --git a/scalewiz/helpers/configuration.py b/scalewiz/helpers/configuration.py index 1e2d2cf..9694dcf 100644 --- a/scalewiz/helpers/configuration.py +++ b/scalewiz/helpers/configuration.py @@ -16,6 +16,18 @@ CONFIG_DIR = Path(user_config_dir("ScaleWiz", "teauxfu")) CONFIG_FILE = Path(CONFIG_DIR, "config.toml") +DATABASE_FILE = Path(CONFIG_DIR, "db.json") + + +def ensure_database() -> None: + """Ensures a database file exists.""" + # make sure we have a place to store data + if DATABASE_FILE.is_file(): + LOGGER.info("Found an existing database file %s", DATABASE_FILE) + else: + LOGGER.info("No database file found. Making one now at %s", DATABASE_FILE) + with DATABASE_FILE.open("w") as dbfile: + dbfile.write("") def ensure_config() -> None: @@ -34,6 +46,8 @@ def ensure_config() -> None: "No config file found in %s. Making one now at %s", CONFIG_DIR, CONFIG_FILE ) init_config() + ensure_database() + update_config("recents", "database", str(DATABASE_FILE)) def init_config() -> None: @@ -84,6 +98,7 @@ def generate_default() -> document: recents = table() recents["analyst"] = "teauxfu" recents["project"] = "" + recents["database"] = str(DATABASE_FILE) doc["recents"] = recents doc["recents"].comment("these will get updated between user sessions") @@ -124,7 +139,6 @@ def generate_default() -> document: def open_config() -> None: """Opens the config file.""" - ensure_config() if CONFIG_FILE.is_file(): os.startfile(CONFIG_FILE) @@ -134,6 +148,8 @@ def get_config() -> dict[str, Union[float, int, str]]: ensure_config() with CONFIG_FILE.open("r") as file: config = loads(file.read()) + if "database" not in config["recents"]: + config["recents"]["database"] = str(DATABASE_FILE) return config @@ -145,7 +161,6 @@ def update_config(table: str, key: str, value: Union[float, int, str]) -> None: key (str): the key to update value (Union[float, int, str]): the new value of `key` """ - ensure_config() doc = loads(CONFIG_FILE.open("r").read()) if table in doc.keys() and key in doc[table].keys(): doc[table][key] = value diff --git a/scalewiz/helpers/score.py b/scalewiz/helpers/score.py index 0f958f0..fb89363 100644 --- a/scalewiz/helpers/score.py +++ b/scalewiz/helpers/score.py @@ -100,7 +100,7 @@ def score(project: Project, log_widget: ScrolledText = None, *args) -> None: f"Result: 1 - ({int_psi} - {baseline_area}) / {avg_protectable_area}" ) log.append(f"Result: {result} \n") - trial.result.set(f"{result:.2f}") + trial.result.set(result) if isinstance(log_widget, tk.Text): to_log(log, log_widget) diff --git a/scalewiz/models/project.py b/scalewiz/models/project.py index a9fdafa..054aadc 100644 --- a/scalewiz/models/project.py +++ b/scalewiz/models/project.py @@ -7,6 +7,7 @@ import tkinter as tk from pathlib import Path from typing import TYPE_CHECKING +from uuid import uuid4 from scalewiz import CONFIG from scalewiz.helpers.configuration import update_config @@ -15,6 +16,7 @@ if TYPE_CHECKING: from typing import List + from uuid import UUID LOGGER = logging.getLogger("scalewiz") @@ -26,6 +28,7 @@ class Project: def __init__(self) -> None: self.tests: List[Test] = [] + self.uuid: str = None # hex string id # experiment parameters that affect score self.baseline = tk.IntVar() self.limit_minutes = tk.DoubleVar() @@ -79,12 +82,47 @@ def set_defaults(self) -> None: self.interval_seconds.set(1) self.analyst.set(CONFIG["recents"]["analyst"]) - def add_traces(self) -> None: - """Adds tkVar traces where needed. Must be cleaned up with remove_traces.""" - self.customer.trace_add("write", self.update_proj_name) - self.client.trace_add("write", self.update_proj_name) - self.field.trace_add("write", self.update_proj_name) - self.sample.trace_add("write", self.update_proj_name) + def get_metadata(self) -> dict: + """Returns a dict representing the Project's metadata. + + Returns: + dict: represents the project's metadata + """ + return { + "customer": self.customer.get(), + "submittedBy": self.submitted_by.get(), + "productionCo": self.client.get(), + "field": self.field.get(), + "sample": self.sample.get(), + "sampleDate": self.sample_date.get(), + "recDate": self.received_date.get(), + "compDate": self.completed_date.get(), + "name": self.name.get(), + "analyst": self.analyst.get(), + "numbers": self.numbers.get(), + "path": str(Path(self.path.get()).resolve()), + "notes": self.notes.get(), + } + + def get_params(self) -> dict: + """Returns a dict representing the Project's experiment parameters. + + Returns: + dict: a dict representing experiment parameters + """ + return { + "bicarbonates": self.bicarbs.get(), + "bicarbsIncreased": self.bicarbs_increased.get(), + "calcium": self.calcium.get(), + "chlorides": self.chlorides.get(), + "baseline": self.baseline.get(), + "temperature": self.temperature.get(), + "limitPSI": self.limit_psi.get(), + "limitMin": self.limit_minutes.get(), + "interval": self.interval_seconds.get(), + "flowrate": self.flowrate.get(), + "uptake": self.uptake_seconds.get(), + } def dump_json(self, path: str = None) -> None: """Dump a JSON representation of the Project at the passed path.""" @@ -97,6 +135,7 @@ def dump_json(self, path: str = None) -> None: label = test.label.get().lower() while label in blanks or label in trials: # make sure we don't overwrite label = "".join((label, " - copy")) + test.label.set(label) if test.is_blank.get(): blanks[label] = test else: @@ -104,7 +143,6 @@ def dump_json(self, path: str = None) -> None: blank_labels = sort_nicely(list(blanks.keys())) trial_labels = sort_nicely(list(trials.keys())) - tests = [] for label in blank_labels: tests.append(blanks.pop(label)) @@ -115,34 +153,8 @@ def dump_json(self, path: str = None) -> None: self.tests = [test for test in tests] this = { - "info": { - "customer": self.customer.get(), - "submittedBy": self.submitted_by.get(), - "productionCo": self.client.get(), - "field": self.field.get(), - "sample": self.sample.get(), - "sampleDate": self.sample_date.get(), - "recDate": self.received_date.get(), - "compDate": self.completed_date.get(), - "name": self.name.get(), - "analyst": self.analyst.get(), - "numbers": self.numbers.get(), - "path": str(Path(self.path.get()).resolve()), - "notes": self.notes.get(), - }, - "params": { - "bicarbonates": self.bicarbs.get(), - "bicarbsIncreased": self.bicarbs_increased.get(), - "calcium": self.calcium.get(), - "chlorides": self.chlorides.get(), - "baseline": self.baseline.get(), - "temperature": self.temperature.get(), - "limitPSI": self.limit_psi.get(), - "limitMin": self.limit_minutes.get(), - "interval": self.interval_seconds.get(), - "flowrate": self.flowrate.get(), - "uptake": self.uptake_seconds.get(), - }, + "info": self.get_metadata(), + "params": self.get_params(), "tests": [test.to_dict() for test in self.tests], "outputFormat": self.output_format.get(), "plot": str(Path(self.plot.get()).resolve()), @@ -170,22 +182,24 @@ def load_json(self, path: str) -> None: "Opened a Project whose actual path didn't match its path property" ) obj["info"]["path"] = str(path) + self.uuid = UUID(obj.get("uuid", uuid4().hex)) + # clerical metadata info: dict = obj["info"] - self.customer.set(info["customer"]) - self.submitted_by.set(info["submittedBy"]) - self.client.set(info["productionCo"]) - self.field.set(info["field"]) - self.sample.set(info["sample"]) - self.sample_date.set(info["sampleDate"]) - self.received_date.set(info["recDate"]) - self.completed_date.set(info["compDate"]) - self.name.set(info["name"]) - self.numbers.set(info["numbers"]) - self.analyst.set(info["analyst"]) + self.customer.set(info.get("customer")) + self.submitted_by.set(info.get("submittedBy")) + self.client.set(info.get("productionCo")) + self.field.set(info.get("field")) + self.sample.set(info.get("sample")) + self.sample_date.set(info.get("sampleDate")) + self.received_date.set(info.get("recDate")) + self.completed_date.set(info.get("compDate")) + self.name.set(info.get("name")) + self.numbers.set(info.get("numbers")) + self.analyst.set(info.get("analyst")) self.path.set(str(Path(info["path"]).resolve())) - self.notes.set(info["notes"]) - + self.notes.set(info.get("notes")) + # experimental metadata defaults = CONFIG["defaults"] params: dict = obj["params"] self.bicarbs.set(params.get("bicarbonates", 0)) @@ -200,14 +214,22 @@ def load_json(self, path: str) -> None: self.flowrate.set(params.get("flowrate", defaults["flowrate"])) self.uptake_seconds.set(params.get("uptake", defaults["uptake_time"])) self.output_format.set(obj.get("outputFormat", defaults["output_format"])) - - self.plot.set(obj["plot"]) + self.plot.set(obj.get("plot")) self.tests.clear() - for entry in obj["tests"]: + for entry in obj.get("tests"): test = Test(data=entry) self.tests.append(test) + # traces / validation stuff + + def add_traces(self) -> None: + """Adds tkVar traces where needed. Must be cleaned up with remove_traces.""" + self.customer.trace_add("write", self.update_proj_name) + self.client.trace_add("write", self.update_proj_name) + self.field.trace_add("write", self.update_proj_name) + self.sample.trace_add("write", self.update_proj_name) + def remove_traces(self) -> None: """Remove tkVar traces to allow the GC to do its thing.""" variables = (self.customer, self.client, self.field, self.sample) diff --git a/scalewiz/models/test.py b/scalewiz/models/test.py index 9fd45f3..bb8194f 100644 --- a/scalewiz/models/test.py +++ b/scalewiz/models/test.py @@ -1,4 +1,4 @@ -"""Model object for a Test.""" +"""Tkinter-powered model object for a Test, with some dict-related capabilities.""" from __future__ import annotations @@ -10,16 +10,36 @@ if TYPE_CHECKING: from typing import List, Tuple, Union + from uuid import UUID LOGGER = logging.getLogger("scalewiz") @dataclass class Reading: - elapsedMin: float + """Holds the data for a particular reading.""" + + elapsed_min: float pump1: int pump2: int - average: int + average: int = round((pump1 + pump2) / 2) + + +# this is currently unused +@dataclass +class TestData: + name: str + label: str + is_blank: bool + on_report: bool + clarity: str + readings: List[Reading] + max_pressure: int + observed_baseline: int + pump_to_score: str + result: float + chemical: str + rate: Union[float, int] class Test: @@ -28,6 +48,7 @@ class Test: # pylint: disable=too-many-instance-attributes def __init__(self, data: dict = None) -> None: + # mutable metadata self.is_blank = tk.BooleanVar() # boolean for blank vs chemical trial self.name = tk.StringVar() # identifier for the test self.chemical = tk.StringVar() # chemical, if any, to be tested @@ -35,14 +56,17 @@ def __init__(self, data: dict = None) -> None: self.label = tk.StringVar() # how the test will be labeled on the report/plot self.clarity = tk.StringVar() # the clarity of the treated water self.notes = tk.StringVar() # misc notes on the experiment + # immutable data + self.readings: Tuple[Reading] = () # list of flat reading dicts + self.uuid: UUID = None + # mutable data self.pump_to_score = tk.StringVar() # which series of PSIs to use self.result = tk.DoubleVar() # represents the test's performance vs the blank self.include_on_report = tk.BooleanVar() # condition for scoring - self.readings: List[Reading] = [] # list of flat reading dicts self.max_psi = tk.IntVar() # the highest psi of the test self.observed_baseline = tk.IntVar() # a guess at the baseline for the test # set defaults - self.pump_to_score.set("pump 1") + self.pump_to_score.set("pump1") self.is_blank.set(True) self.add_traces() # will need to clean these up later for the GC @@ -58,54 +82,63 @@ def add_traces(self) -> None: def to_dict(self) -> dict[str, Union[bool, float, int, str]]: """Returns a dict representation of a Test.""" - self.clean_test() # strip whitespaces from relevant fields + self.strip_test() # strip whitespaces from relevant fields # cast all readings from dataclasses to dicts readings = [] for reading in self.readings: readings.append( { - "pump 1": reading.pump1, - "pump 2": reading.pump2, "average": reading.average, + "pump1": reading.pump1, + "pump2": reading.pump2, "elapsedMin": reading.elapsedMin, } ) return { "name": self.name.get(), - "isBlank": self.is_blank.get(), + "is_blank": self.is_blank.get(), "chemical": self.chemical.get(), "rate": self.rate.get(), - "reportAs": self.label.get(), + "report_as": self.label.get(), "clarity": self.clarity.get(), "notes": self.notes.get(), - "toConsider": self.pump_to_score.get(), - "includeOnRep": self.include_on_report.get(), + "to_consider": self.pump_to_score.get(), + "include_on_report": self.include_on_report.get(), "result": self.result.get(), - "obsBaseline": self.observed_baseline.get(), + "observed_baseline": self.observed_baseline.get(), "readings": readings, } - def load_json(self, obj: dict[str, Union[bool, float, int, str]]) -> None: - """Load a Test with values from a JSON object.""" - self.name.set(obj["name"]) - self.is_blank.set(obj["isBlank"]) - self.chemical.set(obj["chemical"]) - self.rate.set(obj["rate"]) - self.label.set(obj["reportAs"]) - self.clarity.set(obj["clarity"]) - self.notes.set(obj["notes"]) - self.pump_to_score.set(obj["toConsider"]) - self.include_on_report.set(obj["includeOnRep"]) - self.result.set(obj["result"]) - readings = obj["readings"] + def load_json(self, obj: dict) -> None: + """Load a Test with values from a dict.""" + # look for fallbacks for backwards compatability + self.name.set(obj.get("name")) + self.is_blank.set(obj.get("is_blank", obj.get("isBlank"))) + self.calcium.set(obj.get("calcium")) + self.chemical.set(obj.get("chemical")) + self.rate.set(obj.get("rate")) + self.label.set(obj.get("report_as", obj.get("reportAs"))) + self.clarity.set(obj.get("clarity")) + self.notes.set(obj.get("notes")) + self.pump_to_score.set(obj.get("to_consider", obj.get("toConsider"))) + self.include_on_report.set( + obj.get("include_on_report", obj.get("includeOnRep")) + ) + self.result.set(obj.get("result")) + readings: List[dict] = obj.get("readings") for reading in readings: + # do some cleaning for backwards compatibility + pump1 = reading.get("pump1", reading.get("pump 1")) + pump2 = reading.get("pump2", reading.get("pump 2")) + if "average" not in reading.keys(): + average = round((pump1 + pump2) / 2) self.readings.append( Reading( - pump1=reading["pump 1"], - pump2=reading["pump 2"], - average=reading["average"], - elapsedMin=reading["elapsedMin"], + average=average, + pump1=pump1, + pump2=pump2, + elapsed_min=reading.get("elapsed_min", reading.get("elapsedMin")), ) ) self.update_obs_baseline() @@ -113,23 +146,26 @@ def load_json(self, obj: dict[str, Union[bool, float, int, str]]) -> None: def get_readings(self) -> Tuple[int]: """Returns a list of the pump_to_score's pressure readings.""" pump = self.pump_to_score.get() - pump = pump.replace(" ", "") # legacy accomodation for spaces in keys - return [getattr(reading, pump) for reading in self.readings] + if " " in pump: # legacy accomodation for spaces in keys + pump = pump.replace(" ", "") + return tuple([getattr(reading, pump) for reading in self.readings]) def update_test_name(self, *args) -> None: """Makes a name by concatenating the chemical name and rate.""" - if not (self.chemical.get() == "" or self.rate.get() == 0): + if self.chemical.get() != "" and self.rate.get() != 0: if float(self.rate.get()) == int(self.rate.get()): self.name.set(f"{self.chemical.get()} {self.rate.get():.0f} ppm") else: self.name.set(f"{self.chemical.get()} {self.rate.get():.2f} ppm") - def clean_test(self) -> None: + def strip_test(self) -> None: """Do some formatting on the test to clean it up for storing.""" strippables = (self.chemical, self.name, self.label, self.clarity, self.notes) for attr in strippables: - if attr.get().strip() != attr.get(): - attr.set(attr.get().strip()) + val = attr.get() + stripped = val.strip() + if val != stripped: + attr.set(stripped) def update_label(self, *args) -> None: """Sets the label to the current name as a default value.""" diff --git a/scalewiz/models/test_handler.py b/scalewiz/models/test_handler.py index 8a60f0b..e469ea8 100644 --- a/scalewiz/models/test_handler.py +++ b/scalewiz/models/test_handler.py @@ -157,8 +157,9 @@ def get_pressure(pump: NextGenPump) -> Union[float, int]: psi1 = self.pool.submit(get_pressure, self.pump1) psi2 = self.pool.submit(get_pressure, self.pump2) psi1, psi2 = psi1.result(), psi2.result() - t1 = time() - self.logger.warn("got both in %s s", t1 - t0) + self.logger.debug( + "got both pressures from %s in %s s", self.name, time() - t0 + ) average = round(((psi1 + psi2) / 2)) reading = Reading( elapsedMin=self.elapsed_min, pump1=psi1, pump2=psi2, average=average @@ -206,7 +207,7 @@ def save_test(self) -> None: self.logger.info( "Saving %s to %s", self.test.name.get(), self.project.name.get() ) - self.test.readings.extend(self.readings) + self.test.readings = tuple(self.readings) self.project.tests.append(self.test) self.project.dump_json() # refresh data / UI diff --git a/todo b/todo deleted file mode 100644 index 8468c8b..0000000 --- a/todo +++ /dev/null @@ -1,30 +0,0 @@ -todo ----- - -- try to clean up export code / add export confirmation dialog -- handle a queue of changes to a project more gracefully - -bugs ----- - -- none! I'm pretty sure ... - -refactoring ------------ - -- we have a dep. on Pandas for one little call in export helper - could be worked around - -updates / new features ----------------------- - -- none! - -low prio --------- - -- port over the old chlorides / ppm calculators -- check for config missing keys ? -- menubar: - - 'add system' -> 'system' > 'add new', 'remove current' - - this will be a little awkward since we'd have to update/rebuild the menubar - each time a system is added / removed