From a9317eb930fe7befff00570a7a8e61477e804970 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 18:01:44 +0200 Subject: [PATCH 1/8] add validate_date_or_offset --- things/database.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/things/database.py b/things/database.py index 08e17e0..0bf8423 100755 --- a/things/database.py +++ b/things/database.py @@ -253,6 +253,7 @@ def get_tasks( # pylint: disable=R0914,R0917 validate("context_trashed", context_trashed, [None, True, False]) validate("index", index, list(INDICES)) validate_offset("last", last) + validate_date_or_offset("createdat", last) if tag is not None: valid_tags = self.get_tags(titles_only=True) @@ -1243,3 +1244,20 @@ def validate_offset(parameter, argument): "where X is a non-negative integer followed by 'd', 'w', or 'y' " "that indicates days, weeks, or years." ) + + +def validate_date_or_offset(parameter, argument): + errors = [] + try: + validate_date(parameter, argument) + except ValueError as error1: + errors.append(error1) + try: + validate_offset(parameter, argument) + except ValueError as error2: + errors.append(error2) + raise ValueError( + f"Invalid {parameter} argument: {argument!r}\n" + f"Could not validate {argument!r} as date or offset." + ) + From 326f72685fb3a4ba7146c6dba8a96d9003d4374a Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 18:23:44 +0200 Subject: [PATCH 2/8] add createdat parameter --- things/database.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/things/database.py b/things/database.py index 0bf8423..654ab18 100755 --- a/things/database.py +++ b/things/database.py @@ -230,6 +230,7 @@ def get_tasks( # pylint: disable=R0914,R0917 trashed: Optional[bool] = False, context_trashed: Optional[bool] = False, last: Optional[str] = None, + createdat: Optional[str] = None, search_query: Optional[str] = None, index: str = "index", count_only: bool = False, @@ -303,6 +304,7 @@ def get_tasks( # pylint: disable=R0914,R0917 {make_unixtime_filter(f"TASK.{DATE_STOP}", stop_date)} {make_thingsdate_filter(f"TASK.{DATE_DEADLINE}", deadline)} {make_unixtime_range_filter(f"TASK.{DATE_CREATED}", last)} + {make_unixtime_filter(f"TASK.{DATE_CREATED}", createdat)} {make_search_filter(search_query)} """ order_predicate = f'TASK."{index}"' From 7e064ae22fdd8e28d8047357f2880ed410367f06 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 18:40:56 +0200 Subject: [PATCH 3/8] make_unixtime_filter_allowing_range --- things/database.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/things/database.py b/things/database.py index 654ab18..7b987b9 100755 --- a/things/database.py +++ b/things/database.py @@ -4,6 +4,7 @@ import datetime import glob +import math import os import plistlib import re @@ -230,7 +231,7 @@ def get_tasks( # pylint: disable=R0914,R0917 trashed: Optional[bool] = False, context_trashed: Optional[bool] = False, last: Optional[str] = None, - createdat: Optional[str] = None, + created_at: Optional[str] = None, search_query: Optional[str] = None, index: str = "index", count_only: bool = False, @@ -254,7 +255,7 @@ def get_tasks( # pylint: disable=R0914,R0917 validate("context_trashed", context_trashed, [None, True, False]) validate("index", index, list(INDICES)) validate_offset("last", last) - validate_date_or_offset("createdat", last) + validate_date_or_offset("created_at", last) if tag is not None: valid_tags = self.get_tags(titles_only=True) @@ -304,7 +305,7 @@ def get_tasks( # pylint: disable=R0914,R0917 {make_unixtime_filter(f"TASK.{DATE_STOP}", stop_date)} {make_thingsdate_filter(f"TASK.{DATE_DEADLINE}", deadline)} {make_unixtime_range_filter(f"TASK.{DATE_CREATED}", last)} - {make_unixtime_filter(f"TASK.{DATE_CREATED}", createdat)} + {make_unixtime_filter_allowing_range(f"TASK.{DATE_CREATED}", created_at)} {make_search_filter(search_query)} """ order_predicate = f'TASK."{index}"' @@ -1113,6 +1114,15 @@ def make_unixtime_range_filter(date_column: str, offset) -> str: return f"AND {column_datetime} > {offset_datetime}" +def make_unixtime_filter_allowing_range(date_column: str, date_or_offset ): + if date_or_offset is None: + return "" + if isinstance(date_or_offset, bool) or match_date(date_or_offset): + return make_unixtime_filter(date_column, date_or_offset) + else: + return make_unixtime_range_filter(date_column, date_or_offset) + + def match_date(value): """Return a match object if value is an ISO 8601 date str.""" From cc38e0aa8010a0eab3b3ffb7c9b5a182b208dc19 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 18:48:52 +0200 Subject: [PATCH 4/8] add tests for changed_at, copied tests for last --- tests/test_things.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_things.py b/tests/test_things.py index c22d974..3f25e1f 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -325,6 +325,46 @@ def test_last(self): with self.assertRaises(ValueError): things.last("3X") + def test_created_at_date_or_offset(self): + # for offsets it's the same as last: + last_tasks = things.tasks(created_at = "0d") + self.assertEqual(len(last_tasks), 0) + + last_tasks = things.tasks(created_at = "10000w") + self.assertEqual(len(last_tasks), 19) + + last_tasks = things.tasks(created_at = "100y", status="completed") + self.assertEqual(len(last_tasks), 12) + + # for offsets it's the same as last: + last_tasks = things.tasks(created_at = "0d") + self.assertEqual(len(last_tasks), 0) + + last_tasks = things.tasks(created_at = "10000w") + self.assertEqual(len(last_tasks), 19) + + last_tasks = things.tasks(created_at = ">1925-01-01", status="completed") + self.assertEqual(len(last_tasks), 12) + + # None is allowed as it's allowed for dates + last_tasks = things.tasks(created_at = None) + self.assertEqual(len(last_tasks), 19) + + # exceptions are also the same as with last: + with self.assertRaises(TypeError): + things.tasks(created_at = []) + + with self.assertRaises(ValueError): + things.tasks(created_at = "XYZ") + + with self.assertRaises(ValueError): + things.tasks(created_at = "") + + with self.assertRaises(ValueError): + things.tasks(created_at = "3X") + + + def test_tasks(self): count = things.tasks(status="completed", last="100y", count_only=True) self.assertEqual(count, 12) From 39e015e4dc4c3b98ae68cd853191a5a8e24b9440 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 18:51:16 +0200 Subject: [PATCH 5/8] add date tests for creation_date --- tests/test_things.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_things.py b/tests/test_things.py index 3f25e1f..aba345a 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -340,12 +340,18 @@ def test_created_at_date_or_offset(self): last_tasks = things.tasks(created_at = "0d") self.assertEqual(len(last_tasks), 0) + last_tasks = things.tasks(created_at="2025-09-29") + self.assertEqual(len(last_tasks), 0) + last_tasks = things.tasks(created_at = "10000w") self.assertEqual(len(last_tasks), 19) last_tasks = things.tasks(created_at = ">1925-01-01", status="completed") self.assertEqual(len(last_tasks), 12) + last_tasks = things.tasks(created_at=">1825-01-01") + self.assertEqual(len(last_tasks), 19) + # None is allowed as it's allowed for dates last_tasks = things.tasks(created_at = None) self.assertEqual(len(last_tasks), 19) From 5cc15d794eccc2d5fdd5aa9a5470a97e19d43747 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 19:16:15 +0200 Subject: [PATCH 6/8] fix Pydocstyle errors --- tests/test_things.py | 24 +++++++++++------------- things/database.py | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index aba345a..0705167 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -327,49 +327,47 @@ def test_last(self): def test_created_at_date_or_offset(self): # for offsets it's the same as last: - last_tasks = things.tasks(created_at = "0d") + last_tasks = things.tasks(created_at="0d") self.assertEqual(len(last_tasks), 0) - last_tasks = things.tasks(created_at = "10000w") + last_tasks = things.tasks(created_at="10000w") self.assertEqual(len(last_tasks), 19) - last_tasks = things.tasks(created_at = "100y", status="completed") + last_tasks = things.tasks(created_at="100y", status="completed") self.assertEqual(len(last_tasks), 12) # for offsets it's the same as last: - last_tasks = things.tasks(created_at = "0d") + last_tasks = things.tasks(created_at="0d") self.assertEqual(len(last_tasks), 0) last_tasks = things.tasks(created_at="2025-09-29") self.assertEqual(len(last_tasks), 0) - last_tasks = things.tasks(created_at = "10000w") + last_tasks = things.tasks(created_at="10000w") self.assertEqual(len(last_tasks), 19) - last_tasks = things.tasks(created_at = ">1925-01-01", status="completed") + last_tasks = things.tasks(created_at=">1925-01-01", status="completed") self.assertEqual(len(last_tasks), 12) last_tasks = things.tasks(created_at=">1825-01-01") self.assertEqual(len(last_tasks), 19) # None is allowed as it's allowed for dates - last_tasks = things.tasks(created_at = None) + last_tasks = things.tasks(created_at=None) self.assertEqual(len(last_tasks), 19) # exceptions are also the same as with last: with self.assertRaises(TypeError): - things.tasks(created_at = []) + things.tasks(created_at=[]) with self.assertRaises(ValueError): - things.tasks(created_at = "XYZ") + things.tasks(created_at="XYZ") with self.assertRaises(ValueError): - things.tasks(created_at = "") + things.tasks(created_at="") with self.assertRaises(ValueError): - things.tasks(created_at = "3X") - - + things.tasks(created_at="3X") def test_tasks(self): count = things.tasks(status="completed", last="100y", count_only=True) diff --git a/things/database.py b/things/database.py index 7b987b9..fa104b1 100755 --- a/things/database.py +++ b/things/database.py @@ -1114,7 +1114,13 @@ def make_unixtime_range_filter(date_column: str, offset) -> str: return f"AND {column_datetime} > {offset_datetime}" -def make_unixtime_filter_allowing_range(date_column: str, date_or_offset ): + +def make_unixtime_filter_allowing_range(date_column: str, date_or_offset): + """ + Create a SQL filter for a date or an offset. + + Combines make_unixtime_filter and make_unixtime_range_filter in the simplest way possible. + """ if date_or_offset is None: return "" if isinstance(date_or_offset, bool) or match_date(date_or_offset): @@ -1123,7 +1129,6 @@ def make_unixtime_filter_allowing_range(date_column: str, date_or_offset ): return make_unixtime_range_filter(date_column, date_or_offset) - def match_date(value): """Return a match object if value is an ISO 8601 date str.""" return re.fullmatch(r"(=|==|<|<=|>|>=)?(\d{4}-\d{2}-\d{2})", value) @@ -1259,6 +1264,11 @@ def validate_offset(parameter, argument): def validate_date_or_offset(parameter, argument): + """ + For a given date parameter, check if it is either a date or an offset. + + Proof of concept, just combines validate_date and validate_offset. + """ errors = [] try: validate_date(parameter, argument) @@ -1272,4 +1282,3 @@ def validate_date_or_offset(parameter, argument): f"Invalid {parameter} argument: {argument!r}\n" f"Could not validate {argument!r} as date or offset." ) - From 0f443131ba8ed60b60d000971354d80b566b58ca Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 19:16:32 +0200 Subject: [PATCH 7/8] remove unused import --- things/database.py | 1 - 1 file changed, 1 deletion(-) diff --git a/things/database.py b/things/database.py index fa104b1..3f73aaf 100755 --- a/things/database.py +++ b/things/database.py @@ -4,7 +4,6 @@ import datetime import glob -import math import os import plistlib import re From ac367f8b2aea5b51fd43b18369381a9e4d430a89 Mon Sep 17 00:00:00 2001 From: Barne Kleinen Date: Fri, 29 Aug 2025 19:28:37 +0200 Subject: [PATCH 8/8] ignore re-raise error --- things/database.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/things/database.py b/things/database.py index 3f73aaf..6301cc3 100755 --- a/things/database.py +++ b/things/database.py @@ -1124,8 +1124,7 @@ def make_unixtime_filter_allowing_range(date_column: str, date_or_offset): return "" if isinstance(date_or_offset, bool) or match_date(date_or_offset): return make_unixtime_filter(date_column, date_or_offset) - else: - return make_unixtime_range_filter(date_column, date_or_offset) + return make_unixtime_range_filter(date_column, date_or_offset) def match_date(value): @@ -1277,7 +1276,9 @@ def validate_date_or_offset(parameter, argument): validate_offset(parameter, argument) except ValueError as error2: errors.append(error2) - raise ValueError( + raise ValueError( # pylint: disable=W0707 + # re-raising the last error does not really make sense as it both errors apply. + # https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/raise-missing-from.html f"Invalid {parameter} argument: {argument!r}\n" f"Could not validate {argument!r} as date or offset." )