Skip to content

Commit debaf79

Browse files
Add reminder time in output (#133)
Calls to `things.tasks` (and related API endpoints) now include a `reminder_time` if available. This corresponds to the reminder time you can optionally set for to-dos in the Things app.
1 parent 33d77be commit debaf79

File tree

5 files changed

+73
-11
lines changed

5 files changed

+73
-11
lines changed

tests/main.sqlite

0 Bytes
Binary file not shown.

tests/main.sqlite-shm

0 Bytes
Binary file not shown.

tests/main.sqlite-wal

-224 Bytes
Binary file not shown.

tests/test_things.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
import contextlib
66
import io
77
import os
8+
import sqlite3
89
import time
910
import tracemalloc
10-
import sqlite3
1111
import unittest
1212
import unittest.mock
1313

1414
import things
1515
import things.database
1616

17+
1718
tracemalloc.start()
1819

1920
TEST_DATABASE_FILEPATH = "tests/main.sqlite"
@@ -357,21 +358,27 @@ def test_tasks_stopdate_timezones(self):
357358

358359
# make sure we get back both tasks completed for date by midnight UTC+5
359360
# change timezone to Pakistan
360-
os.environ['TZ'] = 'UTC-5' # UTC+5, per https://unix.stackexchange.com/a/104091
361+
os.environ["TZ"] = "UTC-5" # UTC+5, per https://unix.stackexchange.com/a/104091
361362
time.tzset()
362-
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
363+
tasks = things.tasks(
364+
stop_date="2024-06-18", status="completed", count_only=True
365+
)
363366
self.assertEqual(tasks, 2)
364367

365368
# make sure we get back one task completed for date by midnight UTC
366-
os.environ['TZ'] = 'UTC'
369+
os.environ["TZ"] = "UTC"
367370
time.tzset()
368-
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
371+
tasks = things.tasks(
372+
stop_date="2024-06-18", status="completed", count_only=True
373+
)
369374
self.assertEqual(tasks, 1)
370375

371376
# change timezone to New York
372-
os.environ['TZ'] = 'UTC+5' # UTC-5, per https://unix.stackexchange.com/a/104091
377+
os.environ["TZ"] = "UTC+5" # UTC-5, per https://unix.stackexchange.com/a/104091
373378
time.tzset()
374-
tasks = things.tasks(stop_date="2024-06-18", status="completed", count_only=True)
379+
tasks = things.tasks(
380+
stop_date="2024-06-18", status="completed", count_only=True
381+
)
375382
self.assertEqual(tasks, 0)
376383

377384
def test_database_details(self):
@@ -406,11 +413,16 @@ def test_thingsdate(self):
406413
self.assertEqual("AND deadline == 132464128", sqlfilter)
407414
sqlfilter = things.database.make_unixtime_filter("stopDate", "future")
408415
self.assertEqual(
409-
"AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')", sqlfilter
416+
"AND date(stopDate, 'unixepoch', 'localtime') > date('now', 'localtime')",
417+
sqlfilter,
410418
)
411419
sqlfilter = things.database.make_unixtime_filter("stopDate", False)
412420
self.assertEqual("AND stopDate IS NULL", sqlfilter)
413421

422+
def test_thingstime(self):
423+
test_task = things.tasks("7F4vqUNiTvGKaCUfv5pqYG")
424+
self.assertEqual(test_task.get("reminder_time"), "12:34")
425+
414426

415427
if __name__ == "__main__":
416428
unittest.main()

things/database.py

+53-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import plistlib
99
import re
1010
import sqlite3
11-
import weakref
1211
from textwrap import dedent
1312
from typing import Optional, Union
13+
import weakref
1414

1515

1616
# --------------------------------------------------
@@ -75,6 +75,7 @@
7575
"heading_title",
7676
"project",
7777
"project_title",
78+
"reminder_time",
7879
"trashed",
7980
"tags",
8081
)
@@ -107,6 +108,8 @@
107108
# See `convert_isodate_sql_expression_to_thingsdate` for details.
108109
DATE_DEADLINE = "deadline" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary
109110
DATE_START = "startDate" # INTEGER: YYYYYYYYYYYMMMMDDDDD0000000, in binary
111+
# See 'convert_thingstime_sql_expression_to_isotime' for details.
112+
REMINDER_TIME = "reminderTime" # INTEGER: hhhhhmmmmmm00000000000000000000, in binary
110113

111114
# --------------------------------------------------
112115
# Various filters
@@ -528,6 +531,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None):
528531
deadline_expression = convert_thingsdate_sql_expression_to_isodate(
529532
f"TASK.{DATE_DEADLINE}"
530533
)
534+
reminder_time_expression = convert_thingstime_sql_expression_to_isotime(
535+
f"TASK.{REMINDER_TIME}"
536+
)
531537

532538
return f"""
533539
SELECT DISTINCT
@@ -576,8 +582,9 @@ def make_tasks_sql_query(where_predicate=None, order_predicate=None):
576582
CASE
577583
WHEN CHECKLIST_ITEM.uuid IS NOT NULL THEN 1
578584
END AS checklist,
579-
date({start_date_expression}) AS start_date,
580-
date({deadline_expression}) AS deadline,
585+
{start_date_expression} AS start_date,
586+
{deadline_expression} AS deadline,
587+
{reminder_time_expression} AS "reminder_time",
581588
datetime(TASK.{DATE_STOP}, "unixepoch", "localtime") AS "stop_date",
582589
datetime(TASK.{DATE_CREATED}, "unixepoch", "localtime") AS created,
583590
datetime(TASK.{DATE_MODIFIED}, "unixepoch", "localtime") AS modified,
@@ -703,6 +710,49 @@ def convert_thingsdate_sql_expression_to_isodate(sql_expression):
703710
return f"CASE WHEN {thingsdate} THEN {isodate} ELSE {thingsdate} END"
704711

705712

713+
def convert_thingstime_sql_expression_to_isotime(sql_expression: str) -> str:
714+
"""
715+
Return SQL Expression that decodes a Things time as a string.
716+
717+
A _Things time_ is an integer where the binary digits are
718+
hhhhhmmmmmm00000000000000000000; h is hours, m is minutes.
719+
Seconds are not encoded in a Things time.
720+
721+
For example, the ISO 8601 time '12:34:00' corresponds to the Things
722+
time 840957952 as integer; in binary that is:
723+
0110010001000000000000000000000
724+
hhhhhmmmmmm00000000000000000000
725+
12 34 00
726+
727+
Parameters
728+
----------
729+
sql_expression : str
730+
A sql expression pointing to a "Things time" integer
731+
in format hhhhhmmmmmm00000000000000000000, in binary.
732+
733+
Example
734+
-------
735+
>>> convert_thingstime_sql_expression_to_isotime('840957952')
736+
"CASE WHEN 840957952 THEN \
737+
printf('%02d:%02d', (840957952 & 2080374784) >> 26, \
738+
(840957952 & 66060288) >> 20) ELSE 840957952 END"
739+
>>> convert_thingstime_sql_expression_to_isotime('reminderTime')
740+
"CASE WHEN reminderTime THEN \
741+
printf('%02d:%02d', (reminderTime & 2080374784) >> 26, \
742+
(reminderTime & 66060288) >> 20) ELSE reminderTime END"
743+
"""
744+
h_mask = 0b1111100000000000000000000000000
745+
m_mask = 0b0000011111100000000000000000000
746+
747+
thingstime = sql_expression
748+
hours = f"({thingstime} & {h_mask}) >> 26"
749+
minutes = f"({thingstime} & {m_mask}) >> 20"
750+
751+
isotime = f"printf('%02d:%02d', {hours}, {minutes})"
752+
# when thingstime is NULL, return thingstime as-is
753+
return f"CASE WHEN {thingstime} THEN {isotime} ELSE {thingstime} END"
754+
755+
706756
def dict_factory(cursor, row):
707757
"""
708758
Convert SQL result into a dictionary.

0 commit comments

Comments
 (0)