things-py 0.0.4__py3.9.egg → 0.0.6__py3.9.egg
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- EGG-INFO/PKG-INFO +1 -1
- tests/__pycache__/__init__.cpython-39.pyc +0 -0
- tests/__pycache__/test_things.cpython-39.pyc +0 -0
- tests/test_things.py +11 -9
- things/__pycache__/__init__.cpython-39.pyc +0 -0
- things/__pycache__/api.cpython-39.pyc +0 -0
- things/__pycache__/database.cpython-39.pyc +0 -0
- things/api.py +17 -10
- things/database.py +48 -47
EGG-INFO/PKG-INFO
CHANGED
|
Binary file
|
|
Binary file
|
tests/test_things.py
CHANGED
|
@@ -29,7 +29,7 @@ class ThingsCase(unittest.TestCase):
|
|
|
29
29
|
def test_upcoming(self):
|
|
30
30
|
"""Test upcoming."""
|
|
31
31
|
tasks = things.upcoming(**FILEPATH)
|
|
32
|
-
self.assertEqual(
|
|
32
|
+
self.assertEqual(1, len(tasks))
|
|
33
33
|
|
|
34
34
|
def test_deadlines(self):
|
|
35
35
|
"""Test deadlines."""
|
|
@@ -39,7 +39,7 @@ class ThingsCase(unittest.TestCase):
|
|
|
39
39
|
def test_today(self):
|
|
40
40
|
"""Test today."""
|
|
41
41
|
tasks = things.today(**FILEPATH)
|
|
42
|
-
self.assertEqual(
|
|
42
|
+
self.assertEqual(2, len(tasks))
|
|
43
43
|
|
|
44
44
|
def test_checklist(self):
|
|
45
45
|
"""Test checklist."""
|
|
@@ -47,13 +47,12 @@ class ThingsCase(unittest.TestCase):
|
|
|
47
47
|
self.assertEqual(3, len(tasks))
|
|
48
48
|
tasks = things.checklist_items('K9bx7h1xCJdevvyWardZDq', **FILEPATH)
|
|
49
49
|
self.assertEqual(0, len(tasks))
|
|
50
|
-
|
|
51
50
|
|
|
52
51
|
def test_anytime(self):
|
|
53
|
-
"""Test
|
|
52
|
+
"""Test anytime."""
|
|
54
53
|
tasks = things.anytime(**FILEPATH)
|
|
55
|
-
self.assertEqual(
|
|
56
|
-
self.assertTrue(any(task
|
|
54
|
+
self.assertEqual(11, len(tasks))
|
|
55
|
+
self.assertTrue(any(task.pop('area_title', '') == 'Area 1' for task in tasks))
|
|
57
56
|
|
|
58
57
|
def test_logbook(self):
|
|
59
58
|
"""Test logbook."""
|
|
@@ -87,22 +86,25 @@ class ThingsCase(unittest.TestCase):
|
|
|
87
86
|
def test_todos(self):
|
|
88
87
|
"""Test all tasks."""
|
|
89
88
|
tasks = things.todos(start="Anytime", **FILEPATH)
|
|
90
|
-
self.assertEqual(
|
|
89
|
+
self.assertEqual(8, len(tasks))
|
|
91
90
|
tasks = things.todos(start="Anytime", status="completed", **FILEPATH)
|
|
92
91
|
self.assertEqual(6, len(tasks))
|
|
93
92
|
tasks = things.todos(status="completed", **FILEPATH)
|
|
94
93
|
self.assertEqual(10, len(tasks))
|
|
95
94
|
tasks = things.todos(include_items=True, **FILEPATH)
|
|
96
|
-
self.assertEqual(
|
|
95
|
+
self.assertEqual(12, len(tasks))
|
|
97
96
|
with self.assertRaises(ValueError):
|
|
98
97
|
things.todos(status="wrong_value", **FILEPATH)
|
|
99
98
|
tasks = things.tasks("A2oPvtt4dXoypeoLc8uYzY", **FILEPATH)
|
|
100
|
-
self.assertEqual(
|
|
99
|
+
self.assertEqual(16, len(tasks))
|
|
101
100
|
|
|
102
101
|
def test_tags(self):
|
|
103
102
|
"""Test all tags."""
|
|
104
103
|
tags = things.tags(**FILEPATH)
|
|
105
104
|
self.assertEqual(5, len(tags))
|
|
105
|
+
tasks = things.tasks(tag='Errand', **FILEPATH)
|
|
106
|
+
self.assertEqual(1, len(tasks))
|
|
107
|
+
|
|
106
108
|
|
|
107
109
|
def test_projects(self):
|
|
108
110
|
"""Test all projects."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
things/api.py
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
Module implementing Things API.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
__version__ = "0.0.6"
|
|
8
|
+
|
|
7
9
|
import os
|
|
8
10
|
from shlex import quote
|
|
9
11
|
import sys
|
|
@@ -111,8 +113,8 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
|
|
|
111
113
|
|
|
112
114
|
filepath : str, optional
|
|
113
115
|
Any valid path of a SQLite database file generated by the Things app.
|
|
114
|
-
If
|
|
115
|
-
|
|
116
|
+
If the environment variable THINGSDB is set, then that path will be used.
|
|
117
|
+
Otherwise, access the default database path.
|
|
116
118
|
|
|
117
119
|
database : things.database.Database, optional
|
|
118
120
|
Any valid `things.database.Database` object previously instantiated.
|
|
@@ -174,13 +176,6 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
|
|
|
174
176
|
)
|
|
175
177
|
# to-dos without headings appear before headings in app
|
|
176
178
|
items.sort(key=lambda item: item["type"], reverse=True)
|
|
177
|
-
elif task["type"] == "area":
|
|
178
|
-
area = task
|
|
179
|
-
area["items"] = items = tasks(
|
|
180
|
-
area=area["uuid"],
|
|
181
|
-
include_items=True,
|
|
182
|
-
database=database,
|
|
183
|
-
)
|
|
184
179
|
elif task["type"] == "heading":
|
|
185
180
|
heading = task
|
|
186
181
|
heading["items"] = tasks(
|
|
@@ -421,7 +416,8 @@ def checklist_items(uuid, **kwargs):
|
|
|
421
416
|
list of dict
|
|
422
417
|
Representing checklist items. Or [] if none found.
|
|
423
418
|
"""
|
|
424
|
-
return tasks(uuid=uuid, type="to-do", **kwargs).get(
|
|
419
|
+
return tasks(uuid=uuid, type="to-do", **kwargs).get("checklist", [])
|
|
420
|
+
|
|
425
421
|
|
|
426
422
|
# Filter by collections in the Things app sidebar.
|
|
427
423
|
|
|
@@ -497,6 +493,17 @@ def logbook(**kwargs):
|
|
|
497
493
|
return result
|
|
498
494
|
|
|
499
495
|
|
|
496
|
+
def trash(**kwargs):
|
|
497
|
+
"""
|
|
498
|
+
Read trashed tasks into dicts.
|
|
499
|
+
|
|
500
|
+
See `things.api.tasks` for details.
|
|
501
|
+
"""
|
|
502
|
+
result = [*canceled(**kwargs), *completed(**kwargs)]
|
|
503
|
+
result.sort(key=lambda task: task["stop_date"], reverse=True)
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
|
|
500
507
|
# Filter by various task properties
|
|
501
508
|
|
|
502
509
|
|
things/database.py
CHANGED
|
@@ -13,16 +13,16 @@ __author__ = "Alexander Willner"
|
|
|
13
13
|
__copyright__ = "2020 Alexander Willner"
|
|
14
14
|
__credits__ = ["Alexander Willner"]
|
|
15
15
|
__license__ = "Apache License 2.0"
|
|
16
|
-
__version__ = "
|
|
16
|
+
__version__ = "0.0.6"
|
|
17
17
|
__maintainer__ = "Alexander Willner"
|
|
18
18
|
__email__ = "alex@willner.ws"
|
|
19
19
|
__status__ = "Development"
|
|
20
20
|
|
|
21
21
|
import datetime
|
|
22
22
|
import os
|
|
23
|
+
import plistlib
|
|
23
24
|
import sqlite3
|
|
24
25
|
import sys
|
|
25
|
-
from os import environ
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
DEFAULT_DATABASE_FILEPATH = os.path.expanduser(
|
|
@@ -46,7 +46,8 @@ TYPE_TO_FILTER = {"to-do": "type = 0", "project": "type = 1", "heading": "type =
|
|
|
46
46
|
|
|
47
47
|
INDICES = ("index", "todayIndex")
|
|
48
48
|
|
|
49
|
-
COLUMNS_TO_OMIT_IF_NONE = ("area", "area_title", "project", "project_title",
|
|
49
|
+
COLUMNS_TO_OMIT_IF_NONE = ("area", "area_title", "project", "project_title",
|
|
50
|
+
"heading", "heading_title", "trashed", "checklist", "tags")
|
|
50
51
|
COLUMNS_TO_TRANSFORM_TO_BOOL = ("trashed", "checklist", "tags")
|
|
51
52
|
|
|
52
53
|
|
|
@@ -100,7 +101,7 @@ class Database:
|
|
|
100
101
|
|
|
101
102
|
# pylint: disable=R0913
|
|
102
103
|
def __init__(self, filepath=None):
|
|
103
|
-
self.filepath = filepath or environ.get("THINGSDB") or DEFAULT_DATABASE_FILEPATH
|
|
104
|
+
self.filepath = filepath or os.environ.get("THINGSDB") or DEFAULT_DATABASE_FILEPATH
|
|
104
105
|
|
|
105
106
|
# Automated migration to new database location in Things 3.12.6/3.13.1
|
|
106
107
|
# --------------------------------
|
|
@@ -114,22 +115,20 @@ class Database:
|
|
|
114
115
|
|
|
115
116
|
# Core methods
|
|
116
117
|
|
|
117
|
-
def get_tasks(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
search_query=None,
|
|
132
|
-
):
|
|
118
|
+
def get_tasks(self, # pylint: disable=R0914
|
|
119
|
+
uuid=None,
|
|
120
|
+
type=None, # pylint: disable=W0622
|
|
121
|
+
status=None,
|
|
122
|
+
start=None,
|
|
123
|
+
area=None,
|
|
124
|
+
project=None,
|
|
125
|
+
heading=None,
|
|
126
|
+
tag=None,
|
|
127
|
+
start_date=None,
|
|
128
|
+
deadline=None,
|
|
129
|
+
index="index",
|
|
130
|
+
count_only=False,
|
|
131
|
+
search_query=None):
|
|
133
132
|
"""Get tasks. See `api.tasks` for details on parameters."""
|
|
134
133
|
|
|
135
134
|
# Overwrites
|
|
@@ -182,8 +181,8 @@ class Database:
|
|
|
182
181
|
sql_query = self.make_task_sql_query(where_predicate)
|
|
183
182
|
if count_only:
|
|
184
183
|
return self.get_count(sql_query)
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
|
|
185
|
+
return self.execute_query(sql_query)
|
|
187
186
|
|
|
188
187
|
def make_task_sql_query(self, where_predicate):
|
|
189
188
|
"""Make SQL query for Task table"""
|
|
@@ -238,7 +237,9 @@ class Database:
|
|
|
238
237
|
date(TASK.{self.DATE_DEADLINE}, "unixepoch") AS deadline,
|
|
239
238
|
date(TASK.stopDate, "unixepoch") AS "stop_date",
|
|
240
239
|
datetime(TASK.{self.DATE_CREATE}, "unixepoch", "localtime") AS created,
|
|
241
|
-
datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified
|
|
240
|
+
datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified,
|
|
241
|
+
TASK.'index',
|
|
242
|
+
TASK.todayIndex
|
|
242
243
|
FROM
|
|
243
244
|
{self.TABLE_TASK} AS TASK
|
|
244
245
|
LEFT OUTER JOIN
|
|
@@ -262,6 +263,7 @@ class Database:
|
|
|
262
263
|
"""
|
|
263
264
|
|
|
264
265
|
def get_task_rows(self, where_predicate):
|
|
266
|
+
"""Executes SQL query with given WHERE clauses."""
|
|
265
267
|
return self.execute_query(self.make_task_sql_query(where_predicate))
|
|
266
268
|
|
|
267
269
|
def get_areas(self, uuid=None, tag=None, count_only=False):
|
|
@@ -272,11 +274,7 @@ class Database:
|
|
|
272
274
|
valid_tags = self.get_tags(titles_only=True)
|
|
273
275
|
validate("tag", tag, [None] + list(valid_tags))
|
|
274
276
|
|
|
275
|
-
if (
|
|
276
|
-
uuid
|
|
277
|
-
and count_only is False
|
|
278
|
-
and not self.get_areas(uuid=uuid, count_only=True)
|
|
279
|
-
):
|
|
277
|
+
if (uuid and count_only is False and not self.get_areas(uuid=uuid, count_only=True)):
|
|
280
278
|
raise ValueError(f"No such area uuid found: {uuid!r}")
|
|
281
279
|
|
|
282
280
|
# Query
|
|
@@ -349,21 +347,21 @@ class Database:
|
|
|
349
347
|
if titles_only:
|
|
350
348
|
sql_query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
|
|
351
349
|
return self.execute_query(sql_query, row_factory=list_factory)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
350
|
+
|
|
351
|
+
sql_query = f"""
|
|
352
|
+
SELECT
|
|
353
|
+
uuid,
|
|
354
|
+
'tag' AS type,
|
|
355
|
+
title,
|
|
356
|
+
shortcut
|
|
357
|
+
FROM
|
|
358
|
+
{self.TABLE_TAG}
|
|
359
|
+
WHERE
|
|
360
|
+
TRUE
|
|
361
|
+
{make_filter('title', title)}
|
|
362
|
+
ORDER BY "index"
|
|
363
|
+
"""
|
|
364
|
+
return self.execute_query(sql_query)
|
|
367
365
|
|
|
368
366
|
def get_tags_of_task(self, task_uuid):
|
|
369
367
|
"""Get tag titles of task"""
|
|
@@ -401,7 +399,6 @@ class Database:
|
|
|
401
399
|
|
|
402
400
|
def get_version(self):
|
|
403
401
|
"""Get Things Database version."""
|
|
404
|
-
import plistlib
|
|
405
402
|
|
|
406
403
|
sql_query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
|
|
407
404
|
result = self.execute_query(sql_query, row_factory=list_factory)
|
|
@@ -409,6 +406,7 @@ class Database:
|
|
|
409
406
|
return plistlib.loads(plist_bytes)
|
|
410
407
|
|
|
411
408
|
def get_count(self, sql):
|
|
409
|
+
"""Count number of results."""
|
|
412
410
|
sql_query = f"""SELECT COUNT(uuid) FROM ({sql})"""
|
|
413
411
|
return self.execute_query(sql_query, row_factory=list_factory)[0]
|
|
414
412
|
|
|
@@ -420,7 +418,7 @@ class Database:
|
|
|
420
418
|
print(sql_query)
|
|
421
419
|
try:
|
|
422
420
|
uri = f"file:{self.filepath}?mode=ro" # "ro" means read-only
|
|
423
|
-
connection = sqlite3.connect(uri, uri=True)
|
|
421
|
+
connection = sqlite3.connect(uri, uri=True) # pylint: disable=E1101
|
|
424
422
|
connection.row_factory = row_factory or dict_factory
|
|
425
423
|
cursor = connection.cursor()
|
|
426
424
|
cursor.execute(sql_query, parameters)
|
|
@@ -429,7 +427,7 @@ class Database:
|
|
|
429
427
|
for task in tasks:
|
|
430
428
|
print(task)
|
|
431
429
|
return tasks
|
|
432
|
-
except sqlite3.OperationalError as error:
|
|
430
|
+
except sqlite3.OperationalError as error: # pylint: disable=E1101
|
|
433
431
|
print(f"Could not query the database at: {self.filepath}.")
|
|
434
432
|
print(f"Details: {error}.")
|
|
435
433
|
sys.exit(2)
|
|
@@ -437,10 +435,12 @@ class Database:
|
|
|
437
435
|
# -------- Utility methods --------
|
|
438
436
|
|
|
439
437
|
def last_modified(self):
|
|
438
|
+
"""Get last modified time."""
|
|
440
439
|
mtime_seconds = os.path.getmtime(self.filepath)
|
|
441
440
|
return datetime.datetime.fromtimestamp(mtime_seconds)
|
|
442
441
|
|
|
443
442
|
def was_modified_today(self):
|
|
443
|
+
"""Was task modified today?"""
|
|
444
444
|
last_modified_date = self.last_modified().date()
|
|
445
445
|
todays_date = datetime.datetime.now().date()
|
|
446
446
|
return last_modified_date >= todays_date
|
|
@@ -663,7 +663,7 @@ def dict_factory(cursor, row):
|
|
|
663
663
|
return result
|
|
664
664
|
|
|
665
665
|
|
|
666
|
-
def list_factory(
|
|
666
|
+
def list_factory(_cursor, row):
|
|
667
667
|
"""
|
|
668
668
|
Convert SQL selects of one column into a list.
|
|
669
669
|
"""
|
|
@@ -671,6 +671,7 @@ def list_factory(cursor, row):
|
|
|
671
671
|
|
|
672
672
|
|
|
673
673
|
def make_filter(column, value):
|
|
674
|
+
"""Filter SQL query by AND {column} = "{value}"."""
|
|
674
675
|
default = f'AND {column} = "{value}"'
|
|
675
676
|
# special options
|
|
676
677
|
return {
|