5
5
import re
6
6
import sqlite3
7
7
from textwrap import dedent
8
+ import datetime
8
9
9
10
10
11
# --------------------------------------------------
83
84
DATE_DEADLINE = "dueDate"
84
85
DATE_MODIFIED = "userModificationDate"
85
86
DATE_START = "startDate"
87
+ DATE_STOP = "stopDate"
86
88
87
89
# --------------------------------------------------
88
90
# Various filters
@@ -161,7 +163,7 @@ def __init__(self, filepath=None, print_sql=False):
161
163
# Automated migration to new database location in Things 3.12.6/3.13.1
162
164
# --------------------------------
163
165
try :
164
- with open (self .filepath ) as file :
166
+ with open (self .filepath , encoding = "utf-8" ) as file :
165
167
if "Your database file has been moved there" in file .readline ():
166
168
self .filepath = DEFAULT_FILEPATH
167
169
except (UnicodeDecodeError , FileNotFoundError , PermissionError ):
@@ -181,6 +183,7 @@ def get_tasks( # pylint: disable=R0914
181
183
heading = None ,
182
184
tag = None ,
183
185
start_date = None ,
186
+ stop_date = None ,
184
187
deadline = None ,
185
188
deadline_suppressed = None ,
186
189
trashed = False ,
@@ -246,6 +249,7 @@ def get_tasks( # pylint: disable=R0914
246
249
{ make_filter ("TASK.dueDateSuppressionDate" , deadline_suppressed )}
247
250
{ make_filter ("TAG.title" , tag )}
248
251
{ make_date_filter (f"TASK.{ DATE_START } " , start_date )}
252
+ { make_date_filter (f"TASK.{ DATE_STOP } " , stop_date )}
249
253
{ make_date_filter (f"TASK.{ DATE_DEADLINE } " , deadline )}
250
254
{ make_date_range_filter (f"TASK.{ DATE_CREATED } " , last )}
251
255
{ make_search_filter (search_query )}
@@ -637,9 +641,10 @@ def make_date_filter(date_column: str, value) -> str:
637
641
date_column : str
638
642
Name of the column that has date information on a task.
639
643
640
- value : bool, 'future', 'past', or None
644
+ value : bool, 'future', 'past', ISO 8601 date str, or None
641
645
`True` or `False` indicates whether a date is set or not.
642
646
`'future'` or `'past'` indicates a date in the future or past.
647
+ ISO 8601 date str is in the format "YYYY-MM-DD".
643
648
`None` indicates any value.
644
649
645
650
Returns
@@ -659,6 +664,9 @@ def make_date_filter(date_column: str, value) -> str:
659
664
>>> make_date_filter('startDate', 'future')
660
665
"AND date(startDate, 'unixepoch') > date('now', 'localtime')"
661
666
667
+ >>> make_date_filter('stopDate', '2021-03-28')
668
+ "AND date(stopDate, 'unixepoch') >= date('2021-03-28')"
669
+
662
670
>>> make_date_filter('created', None)
663
671
''
664
672
@@ -669,18 +677,25 @@ def make_date_filter(date_column: str, value) -> str:
669
677
if isinstance (value , bool ):
670
678
return make_filter (date_column , value )
671
679
672
- # compare `date_column` to now.
673
- validate ("value" , value , ["future" , "past" ])
680
+ try :
681
+ # Check for ISO 8601 date str
682
+ datetime .date .fromisoformat (value )
683
+ threshold = f"date('{ value } ')"
684
+ comparator = '>='
685
+ except ValueError :
686
+ # "future" or "past"
687
+ validate ("value" , value , ["future" , "past" ])
688
+ threshold = "date('now', 'localtime')"
689
+ comparator = ">" if value == "future" else "<="
690
+
674
691
# Note: not using "localtime" modifier on `date()` since Things.app
675
692
# seems to store `startDate` and `dueDate` as a 00:00 UTC datetime.
676
693
# Morever, note that `stopDate` -- contrary to its name -- seems to
677
694
# be stored as the full UTC datetime of when the task was "stopped".
678
695
# See also: https://github.com/thingsapi/things.py/issues/93
679
696
date = f"date({ date_column } , 'unixepoch')"
680
- operator = ">" if value == "future" else "<="
681
- now = "date('now', 'localtime')"
682
697
683
- return f"AND { date } { operator } { now } "
698
+ return f"AND { date } { comparator } { threshold } "
684
699
685
700
686
701
def make_date_range_filter (date_column , offset ) -> str :
0 commit comments