From e228e30a8dc339b03a543f1272f0ca0d64cee93c Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 12:17:37 +0200 Subject: [PATCH 01/15] extract all status value mappings --- things/database.py | 13 ++-------- things/database_mappings.py | 44 ++++++++++++++++++++++++++++++++ things/database_mappings_test.py | 0 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 things/database_mappings.py create mode 100644 things/database_mappings_test.py diff --git a/things/database.py b/things/database.py index 08e17e0..e6aa4e6 100755 --- a/things/database.py +++ b/things/database.py @@ -11,8 +11,7 @@ from textwrap import dedent from typing import Optional, Union import weakref - - +from .database_mappings import * # -------------------------------------------------- # Core constants # -------------------------------------------------- @@ -43,12 +42,6 @@ "Someday": "start = 2", } -STATUS_TO_FILTER = { - "incomplete": "status = 0", - "canceled": "status = 2", - "completed": "status = 3", -} - TRASHED_TO_FILTER = {True: "trashed = 1", False: "trashed = 0"} TYPE_TO_FILTER = { @@ -121,9 +114,7 @@ IS_HEADING = TYPE_TO_FILTER["heading"] # Status -IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"] -IS_CANCELED = STATUS_TO_FILTER["canceled"] -IS_COMPLETED = STATUS_TO_FILTER["completed"] +# moved to database_mappings # Start IS_INBOX = START_TO_FILTER["Inbox"] diff --git a/things/database_mappings.py b/things/database_mappings.py new file mode 100644 index 0000000..f023c5e --- /dev/null +++ b/things/database_mappings.py @@ -0,0 +1,44 @@ +""" +all repetitions of the status value mapping. + +STATUS_TO_FILTER is used in these forms: + +- in get_tasks: + +list(STATUS_TO_FILTER) + + +start_filter: str = START_TO_FILTER.get(start, "") # type: ignore + status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore + trashed_filter: str = TRASHED_TO_FILTER.get(trashed, "") # type: ignore + + +IS_INCOMPLETE etc is used in these forms: + +- in get_checklist_items +CASE + WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' + WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' + WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' +- in make_tasks_sql_query + CASE + WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' + WHEN TASK.{IS_CANCELED} THEN 'canceled' + WHEN TASK.{IS_COMPLETED} THEN 'completed' +""" + + +STATUS_TO_FILTER = { + "incomplete": "status = 0", + "canceled": "status = 2", + "completed": "status = 3", +} + +# Status +IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"] +IS_CANCELED = STATUS_TO_FILTER["canceled"] +IS_COMPLETED = STATUS_TO_FILTER["completed"] + + + + diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py new file mode 100644 index 0000000..e69de29 From a6a629d2fe80e35bef83da984370ff2403ec026c Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 12:17:52 +0200 Subject: [PATCH 02/15] add new tests --- tests/with_fixtures/testloop.sh | 27 +++++++++++++++++++++++++++ things/database_mappings_test.py | 5 +++++ 2 files changed, 32 insertions(+) create mode 100755 tests/with_fixtures/testloop.sh diff --git a/tests/with_fixtures/testloop.sh b/tests/with_fixtures/testloop.sh new file mode 100755 index 0000000..590a57d --- /dev/null +++ b/tests/with_fixtures/testloop.sh @@ -0,0 +1,27 @@ +#!/bin/zsh + + +echo "-----1: ----$1" +if [ $1 ]; then + subdir=$1 +else + subdir=tasktinder +fi + +# echo "-----subdir: ----$subdir" + +while true; do + clear + pipenv run pytest . + + # pytest parameter + # -x fail after first failur + # -v + # -vv + +# ----- fswatch + # -1 -> exit after one change + fswatch **/*.py -1 + # fswatch lib -1 + +done \ No newline at end of file diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index e69de29..e295ee6 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -0,0 +1,5 @@ +from .database_mappings import * + + +def test_list(): + assert list(STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] \ No newline at end of file From cf79733f5efbc798443d89a7f85b43fd49c0de87 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 12:39:54 +0200 Subject: [PATCH 03/15] replace valid_values_for and add characterization test for status_filter -> make_sql_filter_for --- things/database.py | 2 +- things/database_mappings.py | 30 ++++++++++++++++++++++++------ things/database_mappings_test.py | 17 +++++++++++++++-- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/things/database.py b/things/database.py index e6aa4e6..1afd5e7 100755 --- a/things/database.py +++ b/things/database.py @@ -238,7 +238,7 @@ def get_tasks( # pylint: disable=R0914,R0917 validate("start", start, [None] + list(START_TO_FILTER)) validate_date("start_date", start_date) validate_date("stop_date", stop_date) - validate("status", status, [None] + list(STATUS_TO_FILTER)) + validate("status", status, [None] + valid_values_for("status")) validate("trashed", trashed, [None] + list(TRASHED_TO_FILTER)) validate("type", type, [None] + list(TYPE_TO_FILTER)) validate("context_trashed", context_trashed, [None, True, False]) diff --git a/things/database_mappings.py b/things/database_mappings.py index f023c5e..0a80f9e 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -5,28 +5,46 @@ - in get_tasks: -list(STATUS_TO_FILTER) +[x] list(STATUS_TO_FILTER) -start_filter: str = START_TO_FILTER.get(start, "") # type: ignore - status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore - trashed_filter: str = TRASHED_TO_FILTER.get(trashed, "") # type: ignore +[] status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore IS_INCOMPLETE etc is used in these forms: - in get_checklist_items -CASE +[] CASE WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' - in make_tasks_sql_query - CASE + [] CASE WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' WHEN TASK.{IS_CANCELED} THEN 'canceled' WHEN TASK.{IS_COMPLETED} THEN 'completed' """ +STATUS_VALUE_TO_SQL = { + "incomplete": 0, + "canceled": 2, + "completed": 3, +} + +# the inverse would be used to create test data: +STATUS_VALUE_TO_API = {v: k for k, v in STATUS_VALUE_TO_SQL.items()} + +VALID_VALUES = { + "status" : list(STATUS_VALUE_TO_SQL) +} +def valid_values_for(field): + if field not in VALID_VALUES.keys(): + raise Exception("Only status implemented so far") + return VALID_VALUES[field] + +def make_sql_filter_for(field, value): + return STATUS_TO_FILTER.get(value, "") + STATUS_TO_FILTER = { "incomplete": "status = 0", diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index e295ee6..b0d2bf4 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -1,5 +1,18 @@ -from .database_mappings import * +from .database_mappings import * +import pytest def test_list(): - assert list(STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] \ No newline at end of file + assert list(STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] + +status_test_cases = [ + ("incomplete","status = 0"),("completed","status = 3"),("canceled","status = 2"),("",""),("something else",""), +] + +@pytest.mark.parametrize("status,old_behaviour", status_test_cases) +def test_make_sql_filter_characterization(status,old_behaviour): + assert old_behaviour == STATUS_TO_FILTER.get(status, "") + +@pytest.mark.parametrize("status,expected", status_test_cases) +def test_make_sql_filter(status,expected): + assert make_sql_filter_for("status",status) == expected From b3c155d2f2c8101b6069c33d761b9f55daa507eb Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 12:47:03 +0200 Subject: [PATCH 04/15] replace make_sql_filter_for in status_filter --- things/database.py | 2 +- things/database_mappings.py | 21 +++++++++++++++------ things/database_mappings_test.py | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/things/database.py b/things/database.py index 1afd5e7..7b6b23d 100755 --- a/things/database.py +++ b/things/database.py @@ -254,7 +254,7 @@ def get_tasks( # pylint: disable=R0914,R0917 # See: https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute start_filter: str = START_TO_FILTER.get(start, "") # type: ignore - status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore + status_filter: str = make_sql_filter_for("status", status) # type: ignore trashed_filter: str = TRASHED_TO_FILTER.get(trashed, "") # type: ignore type_filter: str = TYPE_TO_FILTER.get(type, "") # type: ignore diff --git a/things/database_mappings.py b/things/database_mappings.py index 0a80f9e..651233b 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -37,25 +37,34 @@ VALID_VALUES = { "status" : list(STATUS_VALUE_TO_SQL) } + +# these are more generic than needed just for status by taking the field parameter +def value_to_sql(field,value): + if field == "status": + return STATUS_VALUE_TO_SQL[value] + raise NotImplementedError + def valid_values_for(field): if field not in VALID_VALUES.keys(): - raise Exception("Only status implemented so far") + raise NotImplementedError return VALID_VALUES[field] def make_sql_filter_for(field, value): - return STATUS_TO_FILTER.get(value, "") + if value in valid_values_for(field): + return f"{field} = {value_to_sql(field,value)}" + return "" -STATUS_TO_FILTER = { +XXX_STATUS_TO_FILTER = { "incomplete": "status = 0", "canceled": "status = 2", "completed": "status = 3", } # Status -IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"] -IS_CANCELED = STATUS_TO_FILTER["canceled"] -IS_COMPLETED = STATUS_TO_FILTER["completed"] +IS_INCOMPLETE = XXX_STATUS_TO_FILTER["incomplete"] +IS_CANCELED = XXX_STATUS_TO_FILTER["canceled"] +IS_COMPLETED = XXX_STATUS_TO_FILTER["completed"] diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index b0d2bf4..1c747f4 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -3,7 +3,7 @@ import pytest def test_list(): - assert list(STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] + assert list(XXX_STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] status_test_cases = [ ("incomplete","status = 0"),("completed","status = 3"),("canceled","status = 2"),("",""),("something else",""), @@ -11,7 +11,7 @@ def test_list(): @pytest.mark.parametrize("status,old_behaviour", status_test_cases) def test_make_sql_filter_characterization(status,old_behaviour): - assert old_behaviour == STATUS_TO_FILTER.get(status, "") + assert old_behaviour == XXX_STATUS_TO_FILTER.get(status, "") @pytest.mark.parametrize("status,expected", status_test_cases) def test_make_sql_filter(status,expected): From 93d0e03a7a286441445ccb84bafd2da5847f76cd Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 12:59:32 +0200 Subject: [PATCH 05/15] start to prepare replacing status in select statement, BUT THERE ARE NO TESTS FOR IT --- things/database_mappings_test.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 1c747f4..48ac127 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -1,5 +1,5 @@ -from .database_mappings import * +from things.database_mappings import * import pytest def test_list(): @@ -16,3 +16,25 @@ def test_make_sql_filter_characterization(status,old_behaviour): @pytest.mark.parametrize("status,expected", status_test_cases) def test_make_sql_filter(status,expected): assert make_sql_filter_for("status",status) == expected + + +status_field_select_test_cases = [("status", +""" + CASE + WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' + WHEN TASK.{IS_CANCELED} THEN 'canceled' + WHEN TASK.{IS_COMPLETED} THEN 'completed' + END AS status, +""" + +)] + +@pytest.mark.parametrize("field,expected", status_field_select_test_cases) +def test_make_optional_field_select(field, expected): + return """ + CASE + WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' + WHEN TASK.{IS_CANCELED} THEN 'canceled' + WHEN TASK.{IS_COMPLETED} THEN 'completed' + END AS status, + """ From c808a3ca88409284beab601c6ef991f00a8238d7 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:04:09 +0200 Subject: [PATCH 06/15] add test for status mapping - can only check for incomplete --- tests/test_things.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_things.py b/tests/test_things.py index c22d974..a062e23 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -423,6 +423,12 @@ def test_thingstime(self): test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG") self.assertEqual(test_task.get("reminder_time"), "12:34") + def test_status_mapping(self): + tasks = things.tasks() + stati = [t.get("status") for t in tasks] + # there is no testdata in the test db, as they are all incomplete: + assert all(s == 'incomplete' for s in stati) + if __name__ == "__main__": unittest.main() From 59fb65834e5b5a870806dc0e37acf50da67191b1 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:15:43 +0200 Subject: [PATCH 07/15] add test for make_optional_field_select --- things/database_mappings_test.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 48ac127..608ff85 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -18,23 +18,29 @@ def test_make_sql_filter(status,expected): assert make_sql_filter_for("status",status) == expected -status_field_select_test_cases = [("status", -""" +status_field_select_test_cases = [("status","TASK", +f""" CASE WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' WHEN TASK.{IS_CANCELED} THEN 'canceled' WHEN TASK.{IS_COMPLETED} THEN 'completed' END AS status, +"""),("status","CHECKLIST_ITEM", +f""" + CASE + WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' + WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' + WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' + END AS status, """ )] -@pytest.mark.parametrize("field,expected", status_field_select_test_cases) -def test_make_optional_field_select(field, expected): - return """ - CASE - WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' - WHEN TASK.{IS_CANCELED} THEN 'canceled' - WHEN TASK.{IS_COMPLETED} THEN 'completed' - END AS status, - """ + +@pytest.mark.parametrize("field,table,expected", status_field_select_test_cases) +def test_make_optional_field_select(field, table, expected): + canned_answers = { + status_field_select_test_cases[0][1]: status_field_select_test_cases[0][2], + status_field_select_test_cases[1][1]: status_field_select_test_cases[1][2]} + + assert canned_answers[table] == expected From e7d5684d5e7ddebd346823511cf89df85f367c6d Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:19:24 +0200 Subject: [PATCH 08/15] add and use method make_optional_field_select in test --- things/database_mappings.py | 10 ++++++++++ things/database_mappings_test.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/things/database_mappings.py b/things/database_mappings.py index 651233b..33ec937 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -55,6 +55,16 @@ def make_sql_filter_for(field, value): return "" +def make_optional_field_select(field,table): + optional_select = f""" + CASE + WHEN {table}.{IS_INCOMPLETE} THEN 'incomplete' + WHEN {table}.{IS_CANCELED} THEN 'canceled' + WHEN {table}.{IS_COMPLETED} THEN 'completed' + END AS status, +""" + return optional_select + XXX_STATUS_TO_FILTER = { "incomplete": "status = 0", "canceled": "status = 2", diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 608ff85..408e843 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -43,4 +43,4 @@ def test_make_optional_field_select(field, table, expected): status_field_select_test_cases[0][1]: status_field_select_test_cases[0][2], status_field_select_test_cases[1][1]: status_field_select_test_cases[1][2]} - assert canned_answers[table] == expected + assert make_optional_field_select(field,table) == expected From ea8f03f3ff466a38549a48ab12e395f22e7ca67e Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:20:41 +0200 Subject: [PATCH 09/15] use make_sql_filter_for --- things/database_mappings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/things/database_mappings.py b/things/database_mappings.py index 33ec937..3ce9f59 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -58,9 +58,9 @@ def make_sql_filter_for(field, value): def make_optional_field_select(field,table): optional_select = f""" CASE - WHEN {table}.{IS_INCOMPLETE} THEN 'incomplete' - WHEN {table}.{IS_CANCELED} THEN 'canceled' - WHEN {table}.{IS_COMPLETED} THEN 'completed' + WHEN {table}.{make_sql_filter_for(field,'incomplete')} THEN 'incomplete' + WHEN {table}.{make_sql_filter_for(field,'canceled')} THEN 'canceled' + WHEN {table}.{make_sql_filter_for(field,'completed')} THEN 'completed' END AS status, """ return optional_select From b40db78164ba23ad7c71ffbe9d423db5b5f0c68f Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:29:45 +0200 Subject: [PATCH 10/15] generate when clauses --- things/database_mappings.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/things/database_mappings.py b/things/database_mappings.py index 3ce9f59..3114e74 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -44,6 +44,11 @@ def value_to_sql(field,value): return STATUS_VALUE_TO_SQL[value] raise NotImplementedError +def value_to_api(field,value): + if field == "status": + return STATUS_VALUE_TO_API[value] + raise NotImplementedError + def valid_values_for(field): if field not in VALID_VALUES.keys(): raise NotImplementedError @@ -56,11 +61,14 @@ def make_sql_filter_for(field, value): def make_optional_field_select(field,table): + indentation = " " + wcl = [] + for value in valid_values_for(field): + wcl.append(f"{indentation}WHEN {table}.{make_sql_filter_for(field,value)} THEN '{value}'") + when_clauses = "\n".join(wcl) optional_select = f""" CASE - WHEN {table}.{make_sql_filter_for(field,'incomplete')} THEN 'incomplete' - WHEN {table}.{make_sql_filter_for(field,'canceled')} THEN 'canceled' - WHEN {table}.{make_sql_filter_for(field,'completed')} THEN 'completed' +{when_clauses} END AS status, """ return optional_select From ae8327b19bc6884c56323b7c0a1c8093a2f82671 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:37:19 +0200 Subject: [PATCH 11/15] use make_optional_field_select in database.py --- things/database.py | 12 ++---------- things/database_mappings.py | 10 +++++----- things/database_mappings_test.py | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/things/database.py b/things/database.py index 7b6b23d..a3a669d 100755 --- a/things/database.py +++ b/things/database.py @@ -365,11 +365,7 @@ def get_checklist_items(self, todo_uuid=None): sql_query = f""" SELECT CHECKLIST_ITEM.title, - CASE - WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' - WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' - WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' - END AS status, + {make_optional_field_select('status','CHECKLIST_ITEM')} date(CHECKLIST_ITEM.stopDate, "unixepoch", "localtime") AS stop_date, 'checklist-item' as type, CHECKLIST_ITEM.uuid, @@ -538,11 +534,7 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None): WHEN TASK.{IS_TRASHED} THEN 1 END AS trashed, TASK.title, - CASE - WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' - WHEN TASK.{IS_CANCELED} THEN 'canceled' - WHEN TASK.{IS_COMPLETED} THEN 'completed' - END AS status, + {make_optional_field_select('status','TASK')} CASE WHEN AREA.uuid IS NOT NULL THEN AREA.uuid END AS area, diff --git a/things/database_mappings.py b/things/database_mappings.py index 3114e74..31629d7 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -31,7 +31,7 @@ "completed": 3, } -# the inverse would be used to create test data: +# the inverse could be used to create test data: STATUS_VALUE_TO_API = {v: k for k, v in STATUS_VALUE_TO_SQL.items()} VALID_VALUES = { @@ -61,15 +61,15 @@ def make_sql_filter_for(field, value): def make_optional_field_select(field,table): - indentation = " " + indentation = " " wcl = [] for value in valid_values_for(field): - wcl.append(f"{indentation}WHEN {table}.{make_sql_filter_for(field,value)} THEN '{value}'") + wcl.append(f"{indentation} WHEN {table}.{make_sql_filter_for(field,value)} THEN '{value}'") when_clauses = "\n".join(wcl) optional_select = f""" - CASE +CASE {when_clauses} - END AS status, +{indentation}END AS status, """ return optional_select diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 408e843..8a23fe0 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -20,14 +20,14 @@ def test_make_sql_filter(status,expected): status_field_select_test_cases = [("status","TASK", f""" - CASE +CASE WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' WHEN TASK.{IS_CANCELED} THEN 'canceled' WHEN TASK.{IS_COMPLETED} THEN 'completed' END AS status, """),("status","CHECKLIST_ITEM", f""" - CASE +CASE WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' From 42988bd91a502d5f50eea7acfb72ffa79b692946 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 13:39:34 +0200 Subject: [PATCH 12/15] rename the now unused constants --- things/database_mappings.py | 8 +++++--- things/database_mappings_test.py | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/things/database_mappings.py b/things/database_mappings.py index 31629d7..122f403 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -25,6 +25,7 @@ WHEN TASK.{IS_COMPLETED} THEN 'completed' """ +# this is the only place where the mapping is defined STATUS_VALUE_TO_SQL = { "incomplete": 0, "canceled": 2, @@ -73,6 +74,7 @@ def make_optional_field_select(field,table): """ return optional_select +# renamed old constants XXX_STATUS_TO_FILTER = { "incomplete": "status = 0", "canceled": "status = 2", @@ -80,9 +82,9 @@ def make_optional_field_select(field,table): } # Status -IS_INCOMPLETE = XXX_STATUS_TO_FILTER["incomplete"] -IS_CANCELED = XXX_STATUS_TO_FILTER["canceled"] -IS_COMPLETED = XXX_STATUS_TO_FILTER["completed"] +XXX_IS_INCOMPLETE = XXX_STATUS_TO_FILTER["incomplete"] +XXX_IS_CANCELED = XXX_STATUS_TO_FILTER["canceled"] +XXX_IS_COMPLETED = XXX_STATUS_TO_FILTER["completed"] diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 8a23fe0..af26688 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -21,16 +21,16 @@ def test_make_sql_filter(status,expected): status_field_select_test_cases = [("status","TASK", f""" CASE - WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' - WHEN TASK.{IS_CANCELED} THEN 'canceled' - WHEN TASK.{IS_COMPLETED} THEN 'completed' + WHEN TASK.{XXX_IS_INCOMPLETE} THEN 'incomplete' + WHEN TASK.{XXX_IS_CANCELED} THEN 'canceled' + WHEN TASK.{XXX_IS_COMPLETED} THEN 'completed' END AS status, """),("status","CHECKLIST_ITEM", f""" CASE - WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' - WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' - WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' + WHEN CHECKLIST_ITEM.{XXX_IS_INCOMPLETE} THEN 'incomplete' + WHEN CHECKLIST_ITEM.{XXX_IS_CANCELED} THEN 'canceled' + WHEN CHECKLIST_ITEM.{XXX_IS_COMPLETED} THEN 'completed' END AS status, """ From df21a24f1ac37fac155cd83581223c3d87748aec Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 14:26:31 +0200 Subject: [PATCH 13/15] complete test_status_mapping as there is data for the stati in the db --- tests/test_things.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index a062e23..7e2575a 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -424,10 +424,15 @@ def test_thingstime(self): self.assertEqual(test_task.get("reminder_time"), "12:34") def test_status_mapping(self): - tasks = things.tasks() - stati = [t.get("status") for t in tasks] - # there is no testdata in the test db, as they are all incomplete: - assert all(s == 'incomplete' for s in stati) + task = things.tasks(uuid = '5HLnvorXMbqcbjUuPN6ywi') + assert task['title'] == 'Cancelled To-Do in Project' + assert task['status'] == 'canceled' + task = things.tasks(uuid='5u2yGhP4rMQUmPQYEpGYDd') + assert task['title'] == 'Completed To-Do in Project' + assert task['status'] == 'completed' + task = things.tasks(uuid='7F4vqUNiTvGKaCUfv5pqYG') + assert task['title'] == 'To-Do in Upcoming' + assert task['status'] == 'incomplete' if __name__ == "__main__": From 858f817ff28cbfaf9cf4e47152de3294cfb1a617 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 14:41:40 +0200 Subject: [PATCH 14/15] consolidate sql filters one more step --- things/database.py | 3 +-- things/database_mappings.py | 31 ++++++++++++++++++++++--------- things/database_mappings_test.py | 29 ++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/things/database.py b/things/database.py index a3a669d..388f96e 100755 --- a/things/database.py +++ b/things/database.py @@ -254,7 +254,6 @@ def get_tasks( # pylint: disable=R0914,R0917 # See: https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute start_filter: str = START_TO_FILTER.get(start, "") # type: ignore - status_filter: str = make_sql_filter_for("status", status) # type: ignore trashed_filter: str = TRASHED_TO_FILTER.get(trashed, "") # type: ignore type_filter: str = TYPE_TO_FILTER.get(type, "") # type: ignore @@ -282,7 +281,7 @@ def get_tasks( # pylint: disable=R0914,R0917 {project_of_heading_trashed_filter} {type_filter and f"AND TASK.{type_filter}"} {start_filter and f"AND TASK.{start_filter}"} - {status_filter and f"AND TASK.{status_filter}"} + {make_complete_sql_filter_for('status', status)} {make_filter('TASK.uuid', uuid)} {make_filter("TASK.area", area)} {project_filter} diff --git a/things/database_mappings.py b/things/database_mappings.py index 122f403..bfb2a5d 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -8,21 +8,30 @@ [x] list(STATUS_TO_FILTER) -[] status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore +[x] status_filter: str = STATUS_TO_FILTER.get(status, "") # type: ignore IS_INCOMPLETE etc is used in these forms: - in get_checklist_items -[] CASE +[x] CASE WHEN CHECKLIST_ITEM.{IS_INCOMPLETE} THEN 'incomplete' WHEN CHECKLIST_ITEM.{IS_CANCELED} THEN 'canceled' WHEN CHECKLIST_ITEM.{IS_COMPLETED} THEN 'completed' - in make_tasks_sql_query - [] CASE + [x] CASE WHEN TASK.{IS_INCOMPLETE} THEN 'incomplete' WHEN TASK.{IS_CANCELED} THEN 'canceled' WHEN TASK.{IS_COMPLETED} THEN 'completed' + + + +- [x] status_filter is used only once: + +status_filter +status_filter: str = make_sql_filter_for("status", status) # type: ignore +{status_filter and f"AND TASK.{status_filter}"} + """ # this is the only place where the mapping is defined @@ -55,17 +64,21 @@ def valid_values_for(field): raise NotImplementedError return VALID_VALUES[field] -def make_sql_filter_for(field, value): +def _make_sql_filter_for(field, value): if value in valid_values_for(field): return f"{field} = {value_to_sql(field,value)}" return "" +def make_complete_sql_filter_for(field, value): + if value in valid_values_for(field): + return f"AND TASK.{_make_sql_filter_for(field, value)}" + return "" def make_optional_field_select(field,table): indentation = " " wcl = [] for value in valid_values_for(field): - wcl.append(f"{indentation} WHEN {table}.{make_sql_filter_for(field,value)} THEN '{value}'") + wcl.append(f"{indentation} WHEN {table}.{_make_sql_filter_for(field, value)} THEN '{value}'") when_clauses = "\n".join(wcl) optional_select = f""" CASE @@ -75,16 +88,16 @@ def make_optional_field_select(field,table): return optional_select # renamed old constants -XXX_STATUS_TO_FILTER = { +_STATUS_TO_FILTER = { "incomplete": "status = 0", "canceled": "status = 2", "completed": "status = 3", } # Status -XXX_IS_INCOMPLETE = XXX_STATUS_TO_FILTER["incomplete"] -XXX_IS_CANCELED = XXX_STATUS_TO_FILTER["canceled"] -XXX_IS_COMPLETED = XXX_STATUS_TO_FILTER["completed"] +_IS_INCOMPLETE = _STATUS_TO_FILTER["incomplete"] +_IS_CANCELED = _STATUS_TO_FILTER["canceled"] +_IS_COMPLETED = _STATUS_TO_FILTER["completed"] diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index af26688..1b42c87 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -1,9 +1,13 @@ from things.database_mappings import * +from things.database_mappings import (_make_sql_filter_for, _STATUS_TO_FILTER, + _IS_COMPLETED, + _IS_CANCELED, + _IS_INCOMPLETE) # not included in * import pytest def test_list(): - assert list(XXX_STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] + assert list(_STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"] status_test_cases = [ ("incomplete","status = 0"),("completed","status = 3"),("canceled","status = 2"),("",""),("something else",""), @@ -11,26 +15,26 @@ def test_list(): @pytest.mark.parametrize("status,old_behaviour", status_test_cases) def test_make_sql_filter_characterization(status,old_behaviour): - assert old_behaviour == XXX_STATUS_TO_FILTER.get(status, "") + assert old_behaviour == _STATUS_TO_FILTER.get(status, "") @pytest.mark.parametrize("status,expected", status_test_cases) def test_make_sql_filter(status,expected): - assert make_sql_filter_for("status",status) == expected + assert _make_sql_filter_for("status", status) == expected status_field_select_test_cases = [("status","TASK", f""" CASE - WHEN TASK.{XXX_IS_INCOMPLETE} THEN 'incomplete' - WHEN TASK.{XXX_IS_CANCELED} THEN 'canceled' - WHEN TASK.{XXX_IS_COMPLETED} THEN 'completed' + WHEN TASK.{_IS_INCOMPLETE} THEN 'incomplete' + WHEN TASK.{_IS_CANCELED} THEN 'canceled' + WHEN TASK.{_IS_COMPLETED} THEN 'completed' END AS status, """),("status","CHECKLIST_ITEM", f""" CASE - WHEN CHECKLIST_ITEM.{XXX_IS_INCOMPLETE} THEN 'incomplete' - WHEN CHECKLIST_ITEM.{XXX_IS_CANCELED} THEN 'canceled' - WHEN CHECKLIST_ITEM.{XXX_IS_COMPLETED} THEN 'completed' + WHEN CHECKLIST_ITEM.{_IS_INCOMPLETE} THEN 'incomplete' + WHEN CHECKLIST_ITEM.{_IS_CANCELED} THEN 'canceled' + WHEN CHECKLIST_ITEM.{_IS_COMPLETED} THEN 'completed' END AS status, """ @@ -44,3 +48,10 @@ def test_make_optional_field_select(field, table, expected): status_field_select_test_cases[1][1]: status_field_select_test_cases[1][2]} assert make_optional_field_select(field,table) == expected + +def test_complete_status_filter(): + status_filter = _make_sql_filter_for('status', 'completed') + complete_status_filter = status_filter and f"AND TASK.{status_filter}" + assert complete_status_filter == 'AND TASK.status = 3' + expected = 'AND TASK.status = 3' + assert make_complete_sql_filter_for('status','completed') == expected \ No newline at end of file From d24ac76eaaa3ffc682e809b37a6eb555d0218863 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Sun, 31 Aug 2025 14:52:04 +0200 Subject: [PATCH 15/15] some field consolidation --- things/database_mappings.py | 43 +++++++++++++------------------- things/database_mappings_test.py | 17 ++++++++++--- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/things/database_mappings.py b/things/database_mappings.py index bfb2a5d..0de44fb 100644 --- a/things/database_mappings.py +++ b/things/database_mappings.py @@ -35,34 +35,38 @@ """ # this is the only place where the mapping is defined -STATUS_VALUE_TO_SQL = { - "incomplete": 0, - "canceled": 2, - "completed": 3, + +VALUE_TO_SQL = { + 'status': { + "incomplete": 0, + "canceled": 2, + "completed": 3, + } } # the inverse could be used to create test data: -STATUS_VALUE_TO_API = {v: k for k, v in STATUS_VALUE_TO_SQL.items()} - +VALUE_TO_API = {'status' : {v: k for k, v in VALUE_TO_SQL['status'].items()}} +# for more fields, above and below need to be created in a loop over the field names VALID_VALUES = { - "status" : list(STATUS_VALUE_TO_SQL) + "status" : list(VALUE_TO_SQL['status']), } # these are more generic than needed just for status by taking the field parameter def value_to_sql(field,value): - if field == "status": - return STATUS_VALUE_TO_SQL[value] + if field in list(VALUE_TO_SQL): + return VALUE_TO_SQL[field][value] raise NotImplementedError def value_to_api(field,value): - if field == "status": - return STATUS_VALUE_TO_API[value] + if field in list(VALUE_TO_API): + return VALUE_TO_API[field][value] raise NotImplementedError def valid_values_for(field): - if field not in VALID_VALUES.keys(): - raise NotImplementedError - return VALID_VALUES[field] + if field in VALID_VALUES.keys(): + return VALID_VALUES[field] + raise NotImplementedError + def _make_sql_filter_for(field, value): if value in valid_values_for(field): @@ -87,17 +91,6 @@ def make_optional_field_select(field,table): """ return optional_select -# renamed old constants -_STATUS_TO_FILTER = { - "incomplete": "status = 0", - "canceled": "status = 2", - "completed": "status = 3", -} - -# Status -_IS_INCOMPLETE = _STATUS_TO_FILTER["incomplete"] -_IS_CANCELED = _STATUS_TO_FILTER["canceled"] -_IS_COMPLETED = _STATUS_TO_FILTER["completed"] diff --git a/things/database_mappings_test.py b/things/database_mappings_test.py index 1b42c87..8cf40ed 100644 --- a/things/database_mappings_test.py +++ b/things/database_mappings_test.py @@ -1,11 +1,20 @@ from things.database_mappings import * -from things.database_mappings import (_make_sql_filter_for, _STATUS_TO_FILTER, - _IS_COMPLETED, - _IS_CANCELED, - _IS_INCOMPLETE) # not included in * +from things.database_mappings import (_make_sql_filter_for,) # not included in * import pytest +# old constants, kept for testing purposes +_STATUS_TO_FILTER = { + "incomplete": "status = 0", + "canceled": "status = 2", + "completed": "status = 3", +} + +# Status +_IS_INCOMPLETE = _STATUS_TO_FILTER["incomplete"] +_IS_CANCELED = _STATUS_TO_FILTER["canceled"] +_IS_COMPLETED = _STATUS_TO_FILTER["completed"] + def test_list(): assert list(_STATUS_TO_FILTER) == ["incomplete", "canceled", "completed"]