From 1b4c474ad4b51ff8691865bff98b34a1544d76d3 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Fri, 14 Mar 2025 15:29:38 -0400 Subject: [PATCH 01/12] Added first batch of changes to add reminder_time as a returned value. This includes a new convert_thingstime_sql_expression_to_isotime() function, additions to the make_tasks_sql_query function, and adding REMINDER_TIME global variable. --- things/database.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/things/database.py b/things/database.py index 406b28c..45fc29d 100755 --- a/things/database.py +++ b/things/database.py @@ -77,6 +77,7 @@ "project_title", "trashed", "tags", + "reminder_time", ) COLUMNS_TO_TRANSFORM_TO_BOOL = ("checklist", "tags", "trashed") @@ -107,6 +108,7 @@ # See `convert_isodate_sql_expression_to_thingsdate` for details. DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary +REMINDER_TIME = "reminderTime" # INTEGER: HHHHHHmmmmm00000000000000000000, in binary # -------------------------------------------------- # Various filters @@ -528,6 +530,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None): deadline_expression = convert_thingsdate_sql_expression_to_isodate( f"TASK.{DATE_DEADLINE}" ) + reminder_time_expression = convert_thingstime_sql_expression_to_isotime( + f"TASK.{REMINDER_TIME}" + ) return f""" SELECT DISTINCT @@ -578,6 +583,7 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None): END AS checklist, date({start_date_expression}) AS start_date, date({deadline_expression}) AS deadline, + time({reminder_time_expression}) AS reminder_time, datetime(TASK.{DATE_STOP}, "unixepoch", "localtime") AS "stop_date", datetime(TASK.{DATE_CREATED}, "unixepoch", "localtime") AS created, datetime(TASK.{DATE_MODIFIED}, "unixepoch", "localtime") AS modified, @@ -703,6 +709,28 @@ def convert_thingsdate_sql_expression_to_isodate(sql_expression): return f"CASE WHEN {thingsdate} THEN {isodate} ELSE {thingsdate} END" +def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: + """ + Return SQL Expression as a string + + Parameters + ---------- + sql_expression : str + A sql expression pointing to a "Things time" integer in + format HHHHHmmmmmm00000000000000000000, in binary. + See: `convert_isodate_sql_expression_to_thingsdate` for details. + """ + h_mask = 0b11111 + m_mask = 0b111111 + + thingstime = sql_expression + hours = f"({thingstime} & {h_mask}) >> 26" + minutes = f"({thingstime} & {m_mask}) >> 20" + + isotime = f"printf('%02d:%02d', {hours}, {minutes})" + return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END" + + def dict_factory(cursor, row): """ Convert SQL result into a dictionary. From 933f8484142b97fe9edada63d05d7f13049af81a Mon Sep 17 00:00:00 2001 From: John Elliott Date: Sat, 15 Mar 2025 14:24:07 -0400 Subject: [PATCH 02/12] fix bit mask on convert_thingstime... function to return accurate results. --- things/database.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/things/database.py b/things/database.py index 45fc29d..16b21c9 100755 --- a/things/database.py +++ b/things/database.py @@ -108,7 +108,7 @@ # See `convert_isodate_sql_expression_to_thingsdate` for details. DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary -REMINDER_TIME = "reminderTime" # INTEGER: HHHHHHmmmmm00000000000000000000, in binary +REMINDER_TIME = "reminderTime" # INTEGER: HHHHHmmmmmm00000000000000000000, in binary # -------------------------------------------------- # Various filters @@ -720,14 +720,15 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: format HHHHHmmmmmm00000000000000000000, in binary. See: `convert_isodate_sql_expression_to_thingsdate` for details. """ - h_mask = 0b11111 - m_mask = 0b111111 + h_mask = 0b1111100000000000000000000000000 + m_mask = 0b0000011111100000000000000000000 thingstime = sql_expression hours = f"({thingstime} & {h_mask}) >> 26" minutes = f"({thingstime} & {m_mask}) >> 20" isotime = f"printf('%02d:%02d', {hours}, {minutes})" + # when thingstime is NULL, return thingsdate as-is return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END" From 9bca1d3e70983684fbe7b8fc4d542f85b9cb9867 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Mon, 17 Mar 2025 16:13:00 -0400 Subject: [PATCH 03/12] re-added and finalized reminder_time value to make_tasks_sql_query --- things/database.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/things/database.py b/things/database.py index 16b21c9..9f9783e 100755 --- a/things/database.py +++ b/things/database.py @@ -583,7 +583,7 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None): END AS checklist, date({start_date_expression}) AS start_date, date({deadline_expression}) AS deadline, - time({reminder_time_expression}) AS reminder_time, + time({reminder_time_expression}) AS "reminder_time", datetime(TASK.{DATE_STOP}, "unixepoch", "localtime") AS "stop_date", datetime(TASK.{DATE_CREATED}, "unixepoch", "localtime") AS created, datetime(TASK.{DATE_MODIFIED}, "unixepoch", "localtime") AS modified, @@ -1222,3 +1222,7 @@ def validate_offset(parameter, argument): "where X is a non-negative integer followed by 'd', 'w', or 'y' " "that indicates days, weeks, or years." ) + + +if __name__ == "__main__": + print(make_tasks_sql_query()) \ No newline at end of file From 947da5f81c80735af50be781b0076089c453e9c9 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Tue, 18 Mar 2025 08:51:57 -0400 Subject: [PATCH 04/12] Deleted extra line at the bottom of the file. --- things/database.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/things/database.py b/things/database.py index 9f9783e..7be17d2 100755 --- a/things/database.py +++ b/things/database.py @@ -1221,8 +1221,4 @@ def validate_offset(parameter, argument): f"Please specify a string of the format 'X[d/w/y]' " "where X is a non-negative integer followed by 'd', 'w', or 'y' " "that indicates days, weeks, or years." - ) - - -if __name__ == "__main__": - print(make_tasks_sql_query()) \ No newline at end of file + ) \ No newline at end of file From 5b32c12af04395c36d2a7370b02538dbfb47d99e Mon Sep 17 00:00:00 2001 From: John Elliott Date: Mon, 24 Mar 2025 21:27:38 -0400 Subject: [PATCH 05/12] Added function test_thingstime() to verify the return of the correct sql expression --- tests/test_things.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_things.py b/tests/test_things.py index 3f3192a..46ddc32 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -412,6 +412,28 @@ def test_thingsdate(self): self.assertEqual("AND stopDate IS NULL", sqlfilter) + def test_thingstime(self): + sqlfilter: str = "TASK.reminderTime" + self.assertEqual(things.database.convert_thingstime_sql_expression_to_isotime(sqlfilter), f"CASE WHEN TASK.reminderTime THEN printf('%02d:%02d', (TASK.reminderTime & 2080374784) >> 26, (TASK.reminderTime & 66060288) >> 20) ELSE TASK.reminderTime END") + + + ##### Function for testing thingsTime integers ####### + # for task in things.upcoming(): + # if 'reminder_time' in task and task['reminder_time'] != None: + # segments = [int(i) for i in task['reminder_time'].split(":")] + # hours = segments[0] + # minutes = segments[1] + # seconds = segments[2] + # # Check that hours is greater than 0 and less than 24 + # self.assertGreater(hours, 0) + # self.assertLess(hours, 24) + # # Check that minutes is greater than 0 and less than 60 + # self.assertGreater(minutes, 0) + # self.assertLess(minutes, 60) + # # Check that seconds is equal to 0 + # self.assertEqual(seconds, 0) + + if __name__ == "__main__": unittest.main() ThingsCase() # For Vulture From 8441a56d14b2c7e5eaee5cadb3e69fe7bcec1c50 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Mon, 24 Mar 2025 21:33:35 -0400 Subject: [PATCH 06/12] Cleaned up commented lines. --- tests/test_things.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index 46ddc32..cd44e91 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -415,23 +415,6 @@ def test_thingsdate(self): def test_thingstime(self): sqlfilter: str = "TASK.reminderTime" self.assertEqual(things.database.convert_thingstime_sql_expression_to_isotime(sqlfilter), f"CASE WHEN TASK.reminderTime THEN printf('%02d:%02d', (TASK.reminderTime & 2080374784) >> 26, (TASK.reminderTime & 66060288) >> 20) ELSE TASK.reminderTime END") - - - ##### Function for testing thingsTime integers ####### - # for task in things.upcoming(): - # if 'reminder_time' in task and task['reminder_time'] != None: - # segments = [int(i) for i in task['reminder_time'].split(":")] - # hours = segments[0] - # minutes = segments[1] - # seconds = segments[2] - # # Check that hours is greater than 0 and less than 24 - # self.assertGreater(hours, 0) - # self.assertLess(hours, 24) - # # Check that minutes is greater than 0 and less than 60 - # self.assertGreater(minutes, 0) - # self.assertLess(minutes, 60) - # # Check that seconds is equal to 0 - # self.assertEqual(seconds, 0) if __name__ == "__main__": From 9c48cb58e21420cafb8a68f145d58e0859946198 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Fri, 28 Mar 2025 00:02:05 -0400 Subject: [PATCH 07/12] This commit includes changes to all of the proposed improvements from the previous pull request as well as an updated version of the test_thingstime() function that only checks a single task in the test DB. The task with uuid 7F4vqUNiTvGKaCUfv5pqYG has been updated in the test .thingsdatabase files to include a reminder_time value that decodes as 12:34:00. The updated databse files are included in this commit as wel. --- tests/main.sqlite | Bin 180224 -> 180224 bytes tests/main.sqlite-shm | Bin 32768 -> 32768 bytes tests/main.sqlite-wal | Bin 32992 -> 32768 bytes tests/test_things.py | 5 ++--- things/database.py | 33 +++++++++++++++++++++++++++------ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/main.sqlite b/tests/main.sqlite index 3d80f982739c01b616b3ed3bff09b86ed1e64e21..37a11f3067cf3bb3ea1704f0aac8aad6d944f118 100644 GIT binary patch delta 306 zcmZo@;BIK(-oR+U#=yW}#OFMj-5`nChtGLq<8@v!4rUg{w1RUCMm)^yjE*@>*wRJmk(8CkQeU3Yl>k^kY=XFkhj*}ct z?0Xmm*!9_#vT?BHuv}s>XI{rx!)(DcoALj|MlX>@Ms_Vr4t9194h9Z=MNS?@77mu_ z6KWYHr_ZTjlxM&Bv-O6^AMRWW2> zU|?m~9BJ2lkm*I{=6nA&3T!4a@b~j`@%8g@^VaaZ=PBU6&+X53iz}G(3}*z#1r9Iv z!|b*U6WKPfsj+slyk!Yy-odQT@Rw;PlMG|S#76(^0)>qCxp`~ZwJbT<**Q2EIP?{# z>xweTPv2Rx5b^1IpbDm{On)oLMpLQKmznO3 delta 205 zcmZo@U}|V!s+V}A%K!t63=9GsK#mR&CzgFL4}W;!rowloYYEwVEM?E$Qe7mY%u1?y znAsq6|04mYxEcdU0t8rqSP6*+vEg{*M0@v*3q)9$85x*2PCO{d%>-1>%D~3J&cLy8 HqNNl7QcN(m diff --git a/tests/main.sqlite-wal b/tests/main.sqlite-wal index ed01ba906a6cb3a3844948d0a3ecb55ca6f37705..2cfd56424ecb4e1d964847e7c0d7203680c08524 100644 GIT binary patch delta 3296 zcmai0du$tb9lyJ{v7Ps;ZC*{Ct}0r$Qf$XBjnJK6=Q{Sy&c$|YtzqZ$`5F8CI6HB= z%E`7)ql;F`{S&nPu@&thZ3J~l6vo&eMJn5n5EKL{k7|OkiUI067^9`Nx!=!;oiq;k z4}YDD^!a>0pYP+oA1q;ui&#llYJLJa^2dGz=|aR0ruZCN8#z5OzWSHj>R0x8|0O;X z|A#!d@qGN!N7rwzeO=QoKHP^aBb{eETRP5mw6;Iqb^}>%n`!-7tH0$+%Y^(l@<(Lv z$&Avs(JtxH=9imgyJjUh<0TATe_bh!&sPwQwCY8!$i0tU7ybYDe zB*ITE10|hxtdQ}Lv8e(vL7P0`f+n5u6Zl%c=9uTI7r%Pz^jhB~`4o4=%qe0ig(uB$ z$yh4li}BGGlH=JNCjEG2QKdkTvZ{O%{H|2cuElr{q?3zuns#}c9Fc)cWFkHIpmG$2 zedKbiALI!K#M}?bYZ#%jH z2hqyrJtcs0>PsKZ;#UWM|8wufwGA{;0svAd@0869kq>1_LYbPRopx8m-z=$e z4D74=g-T_(=+}k-6bLXNC<>K`Ux0BkGZ|CsBhL9LYccAIs-~4+DA<%Ski(PYH`hQs zT{>vRzz_Pq*B&;}DTbY$WkU*~TQP|KKseQ@}NRovJ5AYYih`L~`eDWY>W9g9C9fDYbzNV^?%>z|gBFh7#!ubCExLcPe z4MlZ4tuaLdI1vxnqt37&U=;uM{uB7x;0G_hdgsMj%T?=DZ@BU$;jfPj_xJbL>^io& zK*9xE-yx%Kfhp<&lv_JBrpKrCg&gfN=^@46dsYF(OK-gVg77=%aQD6NZvyo8y?ZyI z3p4%u4|(QDi$moLxu-$~Rlq^3YzbS6sY0gi0Lb&F4j6?qfCz zKcva4ibT{kmUX&4NtfGiNfUl^2q5O7r*4CoAHDoffY`I%zI7A+AkNIb*4=eTq0^~2 zq_H_t1ui@73}%>M+>>{a+TtAS!<>8f0zl+r=l%m^_ijV142Y1!ZPLCfrdX~c0Mlqu z>iLK!!=@Lq?u5avjl21v#ssjkif^sJY)7tLs8wqlZaE5ZTY5Kgl^cW;{iEifUK5-O zcQ*_wF6E<#}!sG)x0-EkyVs(V5UjoN5Oe%u4RjP8*exa6W6`C{;P8nUvD6HChAS(!oeKwrODKsQ8}+R z<;_6ugxXDm=yY4p*G%2cnJUMsD%<5Xm8R+;0_bEiRQQFZw}hL;R#94#_60{euOe*o z)`FwJ0Q}=iBL_eXja)wr?G26m>7fSp?r@>JUOP>eI%JJAz$A$9OG|gjVNv-k#5_(W zd7ICyW(*Nc%!dPR+vR^81d(BW^dYpIVczO#VEGO?ROaeQv^3X}7zeK}$@+nWNrWqw zo)WbXnGX9Sq<1XgOKa#|?)N~m$QPVLmD#@Gx|%&GzRLpL-hU=Aw>o`XKn zD}yTyd}x$IfBDDtEKtdoEW|m6%>b{gQmqNsd(dDmE<_xOKv>Bb%|W{=RNynCxI7{fLR7DkWv3hxkYtRs@~;BGub z<|ws^uyKhoOWXmc>S*R0pF(BGciyc!z2%(SNm=YufB^&)zB64*$lhbQ>g6xvIeoSkKp_@n@hM+UpPw@A zl?fN0JA)CQ)#QAI6h^Df4;4=FPMp9Z#KX+a=y-F^?xfW{hm3&QSQ$2N(L2Jp*+XDQ J{^SDr2LRavC`kYS diff --git a/tests/test_things.py b/tests/test_things.py index cd44e91..85b07d9 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -413,9 +413,8 @@ def test_thingsdate(self): def test_thingstime(self): - sqlfilter: str = "TASK.reminderTime" - self.assertEqual(things.database.convert_thingstime_sql_expression_to_isotime(sqlfilter), f"CASE WHEN TASK.reminderTime THEN printf('%02d:%02d', (TASK.reminderTime & 2080374784) >> 26, (TASK.reminderTime & 66060288) >> 20) ELSE TASK.reminderTime END") - + test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG")['reminder_time'] + self.assertEqual(test_task, "12:34:00") if __name__ == "__main__": unittest.main() diff --git a/things/database.py b/things/database.py index 7be17d2..0ade876 100755 --- a/things/database.py +++ b/things/database.py @@ -75,9 +75,9 @@ "heading_title", "project", "project_title", + "reminder_time", "trashed", "tags", - "reminder_time", ) COLUMNS_TO_TRANSFORM_TO_BOOL = ("checklist", "tags", "trashed") @@ -108,7 +108,8 @@ # See `convert_isodate_sql_expression_to_thingsdate` for details. DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary -REMINDER_TIME = "reminderTime" # INTEGER: HHHHHmmmmmm00000000000000000000, in binary +# See 'convert_isodate_sql_expresstion_to_thingstime' for details. +REMINDER_TIME = "reminderTime" # INTEGER: hhhhhmmmmmm00000000000000000000, in binary # -------------------------------------------------- # Various filters @@ -711,15 +712,35 @@ def convert_thingsdate_sql_expression_to_isodate(sql_expression): def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: """ - Return SQL Expression as a string + Return SQL Expression that decodes a Things time as a string. + + A _Things time_ is an integer where the binary digits are + hhhhhmmmmmm00000000000000000000; h is hours, m is minutes. + Seconds are not encoded in a Things time. + + For example, the ISO 8601 time '12:34:00' corresponds to the Things + time 840957952 as integer; in binary that is: + 1100100010000000000000000000000 + hhhhhmmmmmm00000000000000000000 + 12 34 00 Parameters ---------- sql_expression : str A sql expression pointing to a "Things time" integer in - format HHHHHmmmmmm00000000000000000000, in binary. - See: `convert_isodate_sql_expression_to_thingsdate` for details. + format hhhhhmmmmmm00000000000000000000, in binary. + + + + Example + ------- + >>> convert_thingstime_sql_expression_to_isotime(TASK.reminderTime) + "time(CASE WHEN TASK.reminderTime THEN \ + printf('%02d:%02d', (TASK.reminderTime & 2080374784) >> 26, \ + (TASK.reminderTime & 66060288) >> 20) \ + ELSE TASK.reminderTime END) AS "reminder_time"", """ + h_mask = 0b1111100000000000000000000000000 m_mask = 0b0000011111100000000000000000000 @@ -728,7 +749,7 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: minutes = f"({thingstime} & {m_mask}) >> 20" isotime = f"printf('%02d:%02d', {hours}, {minutes})" - # when thingstime is NULL, return thingsdate as-is + # when thingstime is NULL, return thingstime as-is return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END" From 95ec4951f5dd2a7b5a379a6c6eb3d0642e12e6a8 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Sun, 30 Mar 2025 10:15:10 -0500 Subject: [PATCH 08/12] Removed the Sun Mar 30 10:15:10 CDT 2025 and SQL functions from make_tasks_sql_query() where they were wrapping the deadline, start_date, and reminder_time expressions. FIX: This commit also fixes the missing prefixed zero in the description of the reminderTime and the incorrect function name in the comment on line 111. --- tests/test_things.py | 4 ++-- things/database.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index 85b07d9..b770f9a 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -413,8 +413,8 @@ def test_thingsdate(self): def test_thingstime(self): - test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG")['reminder_time'] - self.assertEqual(test_task, "12:34:00") + test_task = things.get("7F4vqUNiTvGKaCUfv5pqYG")['reminder_time'] + self.assertEqual(test_task, "12:34") if __name__ == "__main__": unittest.main() diff --git a/things/database.py b/things/database.py index 0ade876..3109ba6 100755 --- a/things/database.py +++ b/things/database.py @@ -108,7 +108,7 @@ # See `convert_isodate_sql_expression_to_thingsdate` for details. DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary -# See 'convert_isodate_sql_expresstion_to_thingstime' for details. +# See 'convert_thingstime_sql_expression_to_isotime' for details. REMINDER_TIME = "reminderTime" # INTEGER: hhhhhmmmmmm00000000000000000000, in binary # -------------------------------------------------- @@ -582,9 +582,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None): CASE WHEN CHECKLIST_ITEM.uuid IS NOT NULL THEN 1 END AS checklist, - date({start_date_expression}) AS start_date, - date({deadline_expression}) AS deadline, - time({reminder_time_expression}) AS "reminder_time", + {start_date_expression} AS start_date, + {deadline_expression} AS deadline, + {reminder_time_expression} AS "reminder_time", datetime(TASK.{DATE_STOP}, "unixepoch", "localtime") AS "stop_date", datetime(TASK.{DATE_CREATED}, "unixepoch", "localtime") AS created, datetime(TASK.{DATE_MODIFIED}, "unixepoch", "localtime") AS modified, @@ -720,7 +720,7 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: For example, the ISO 8601 time '12:34:00' corresponds to the Things time 840957952 as integer; in binary that is: - 1100100010000000000000000000000 + 0110010001000000000000000000000 hhhhhmmmmmm00000000000000000000 12 34 00 From fda3db9b9929944cac413695df5c2f40883148c3 Mon Sep 17 00:00:00 2001 From: John Elliott Date: Sun, 30 Mar 2025 14:07:25 -0500 Subject: [PATCH 09/12] This commit removes new line characters from the description of and fixes the input and output in the same description. It is also includes a change in test_things.py to the function such that things.get function is called and the 'reminder_time' on the next line instead of at the moment the function is called. --- tests/test_things.py | 30 +++++++++++++++++++----------- things/database.py | 20 +++++++------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index b770f9a..6f76de5 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -5,15 +5,16 @@ import contextlib import io import os +import sqlite3 import time import tracemalloc -import sqlite3 import unittest import unittest.mock import things import things.database + tracemalloc.start() TEST_DATABASE_FILEPATH = "tests/main.sqlite" @@ -357,21 +358,27 @@ def test_tasks_stopdate_timezones(self): # make sure we get back both tasks completed for date by midnight UTC+5 # change timezone to Pakistan - os.environ['TZ'] = 'UTC-5' # UTC+5, per https://unix.stackexchange.com/a/104091 + os.environ["TZ"] = "UTC-5" # UTC+5, per https://unix.stackexchange.com/a/104091 time.tzset() - tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True) + tasks = things.tasks( + stop_date="2024-06-18", status="completed", count_only=True + ) self.assertEqual(tasks, 2) # make sure we get back one task completed for date by midnight UTC - os.environ['TZ'] = 'UTC' + os.environ["TZ"] = "UTC" time.tzset() - tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True) + tasks = things.tasks( + stop_date="2024-06-18", status="completed", count_only=True + ) self.assertEqual(tasks, 1) # change timezone to New York - os.environ['TZ'] = 'UTC+5' # UTC-5, per https://unix.stackexchange.com/a/104091 + os.environ["TZ"] = "UTC+5" # UTC-5, per https://unix.stackexchange.com/a/104091 time.tzset() - tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True) + tasks = things.tasks( + stop_date="2024-06-18", status="completed", count_only=True + ) self.assertEqual(tasks, 0) def test_database_details(self): @@ -406,15 +413,16 @@ def test_thingsdate(self): self.assertEqual("AND deadline == 132464128", sqlfilter) sqlfilter = things.database.make_unixtime_filter("stopDate", "future") self.assertEqual( - "AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')", sqlfilter + "AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')", + sqlfilter, ) sqlfilter = things.database.make_unixtime_filter("stopDate", False) self.assertEqual("AND stopDate IS NULL", sqlfilter) - def test_thingstime(self): - test_task = things.get("7F4vqUNiTvGKaCUfv5pqYG")['reminder_time'] - self.assertEqual(test_task, "12:34") + test_task = things.get("7F4vqUNiTvGKaCUfv5pqYG") + self.assertEqual(test_task['reminder_time'], "12:34") + if __name__ == "__main__": unittest.main() diff --git a/things/database.py b/things/database.py index 3109ba6..5a27f71 100755 --- a/things/database.py +++ b/things/database.py @@ -8,9 +8,9 @@ import plistlib import re import sqlite3 -import weakref from textwrap import dedent from typing import Optional, Union +import weakref # -------------------------------------------------- @@ -108,7 +108,7 @@ # See `convert_isodate_sql_expression_to_thingsdate` for details. DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary -# See 'convert_thingstime_sql_expression_to_isotime' for details. +# See 'convert_thingstime_sql_expression_to_isotime' for details. REMINDER_TIME = "reminderTime" # INTEGER: hhhhhmmmmmm00000000000000000000, in binary # -------------------------------------------------- @@ -727,20 +727,14 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: Parameters ---------- sql_expression : str - A sql expression pointing to a "Things time" integer in - format hhhhhmmmmmm00000000000000000000, in binary. - + A sql expression pointing to a "Things time" integer in format hhhhhmmmmmm00000000000000000000, in binary. - Example ------- - >>> convert_thingstime_sql_expression_to_isotime(TASK.reminderTime) - "time(CASE WHEN TASK.reminderTime THEN \ - printf('%02d:%02d', (TASK.reminderTime & 2080374784) >> 26, \ - (TASK.reminderTime & 66060288) >> 20) \ - ELSE TASK.reminderTime END) AS "reminder_time"", + >>> convert_thingstime_sql_expression_to_isotime(840957952) + "CASE WHEN 840957952 THEN printf('%02d:%02d', (840957952 & 2080374784) >> 26, (840957952 & 66060288) >> 20) ELSE 840957952 END" """ - + h_mask = 0b1111100000000000000000000000000 m_mask = 0b0000011111100000000000000000000 @@ -748,7 +742,7 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: hours = f"({thingstime} & {h_mask}) >> 26" minutes = f"({thingstime} & {m_mask}) >> 20" - isotime = f"printf('%02d:%02d', {hours}, {minutes})" + isotime = f"printf('%02d:%02d', {hours}, {minutes})" # when thingstime is NULL, return thingstime as-is return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END" From 941bf90e2eb36be3a459ed5e59a779e55da1e34f Mon Sep 17 00:00:00 2001 From: John Elliott Date: Tue, 1 Apr 2025 01:06:51 -0400 Subject: [PATCH 10/12] This push includes changes to the docstring for to make sure it shows the correct inputs and outputs in the examples and also fits better stylistically. It also includes a change to to ensure that key values are handled correctly in instances where the task has no reminder_time key. --- tests/test_things.py | 2 +- things/database.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index 6f76de5..a280a56 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -421,7 +421,7 @@ def test_thingsdate(self): def test_thingstime(self): test_task = things.get("7F4vqUNiTvGKaCUfv5pqYG") - self.assertEqual(test_task['reminder_time'], "12:34") + self.assertEqual(test_task.get('reminder_time'), "12:34") if __name__ == "__main__": diff --git a/things/database.py b/things/database.py index 5a27f71..c387fd8 100755 --- a/things/database.py +++ b/things/database.py @@ -727,12 +727,19 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: Parameters ---------- sql_expression : str - A sql expression pointing to a "Things time" integer in format hhhhhmmmmmm00000000000000000000, in binary. + A sql expression pointing to a "Things time" integer + in format hhhhhmmmmmm00000000000000000000, in binary. Example ------- - >>> convert_thingstime_sql_expression_to_isotime(840957952) - "CASE WHEN 840957952 THEN printf('%02d:%02d', (840957952 & 2080374784) >> 26, (840957952 & 66060288) >> 20) ELSE 840957952 END" + >>> convert_thingstime_sql_expression_to_isotime('840957952') + "CASE WHEN 840957952 THEN \ + printf('%02d:%02d', (840957952 & 2080374784) >> 26, \ + (840957952 & 66060288) >> 20) ELSE 840957952 END" + >>> convert_thingstime_sql_expression_to_isotime('reminderTime') + "CASE WHEN reminderTime THEN \ + printf('%02d:%02d', (reminderTime & 2080374784) >> 26, \ + (reminderTime & 66060288) >> 20) ELSE reminderTime END" """ h_mask = 0b1111100000000000000000000000000 From 5c2d91dd2c06c24895b4f88cbe554fb0283973ae Mon Sep 17 00:00:00 2001 From: Michael B Date: Tue, 1 Apr 2025 16:26:38 +0200 Subject: [PATCH 11/12] minor style tweaks - use `.tasks` instead of `.get` - missing newline - use "black"-style `make auto-style` double-quotes instead of single-quotes --- tests/test_things.py | 4 ++-- things/database.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_things.py b/tests/test_things.py index a280a56..c22d974 100644 --- a/tests/test_things.py +++ b/tests/test_things.py @@ -420,8 +420,8 @@ def test_thingsdate(self): self.assertEqual("AND stopDate IS NULL", sqlfilter) def test_thingstime(self): - test_task = things.get("7F4vqUNiTvGKaCUfv5pqYG") - self.assertEqual(test_task.get('reminder_time'), "12:34") + test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG") + self.assertEqual(test_task.get("reminder_time"), "12:34") if __name__ == "__main__": diff --git a/things/database.py b/things/database.py index c387fd8..9f04d15 100755 --- a/things/database.py +++ b/things/database.py @@ -1243,4 +1243,4 @@ def validate_offset(parameter, argument): f"Please specify a string of the format 'X[d/w/y]' " "where X is a non-negative integer followed by 'd', 'w', or 'y' " "that indicates days, weeks, or years." - ) \ No newline at end of file + ) From b336967ee03a79b49d9433639d3207cf2f8d3ae7 Mon Sep 17 00:00:00 2001 From: Michael B Date: Tue, 1 Apr 2025 16:31:46 +0200 Subject: [PATCH 12/12] remove extra whitespace --- things/database.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/things/database.py b/things/database.py index 9f04d15..08e17e0 100755 --- a/things/database.py +++ b/things/database.py @@ -714,12 +714,12 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: """ Return SQL Expression that decodes a Things time as a string. - A _Things time_ is an integer where the binary digits are + A _Things time_ is an integer where the binary digits are hhhhhmmmmmm00000000000000000000; h is hours, m is minutes. - Seconds are not encoded in a Things time. + Seconds are not encoded in a Things time. - For example, the ISO 8601 time '12:34:00' corresponds to the Things - time 840957952 as integer; in binary that is: + For example, the ISO 8601 time '12:34:00' corresponds to the Things + time 840957952 as integer; in binary that is: 0110010001000000000000000000000 hhhhhmmmmmm00000000000000000000 12 34 00 @@ -727,21 +727,20 @@ def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str: Parameters ---------- sql_expression : str - A sql expression pointing to a "Things time" integer + A sql expression pointing to a "Things time" integer in format hhhhhmmmmmm00000000000000000000, in binary. - + Example ------- - >>> convert_thingstime_sql_expression_to_isotime('840957952') + >>> convert_thingstime_sql_expression_to_isotime('840957952') "CASE WHEN 840957952 THEN \ printf('%02d:%02d', (840957952 & 2080374784) >> 26, \ - (840957952 & 66060288) >> 20) ELSE 840957952 END" + (840957952 & 66060288) >> 20) ELSE 840957952 END" >>> convert_thingstime_sql_expression_to_isotime('reminderTime') "CASE WHEN reminderTime THEN \ printf('%02d:%02d', (reminderTime & 2080374784) >> 26, \ (reminderTime & 66060288) >> 20) ELSE reminderTime END" """ - h_mask = 0b1111100000000000000000000000000 m_mask = 0b0000011111100000000000000000000