things-py 0.0.2__tar.gz → 0.0.3__tar.gz
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.
- {things.py-0.0.2 → things.py-0.0.3}/PKG-INFO +2 -2
- {things.py-0.0.2 → things.py-0.0.3}/README.md +1 -1
- {things.py-0.0.2 → things.py-0.0.3}/setup.py +8 -8
- {things.py-0.0.2 → things.py-0.0.3}/tests/test_things.py +5 -5
- {things.py-0.0.2 → things.py-0.0.3}/things/__init__.py +1 -1
- {things.py-0.0.2 → things.py-0.0.3}/things/api.py +42 -20
- {things.py-0.0.2 → things.py-0.0.3}/things/database.py +128 -118
- {things.py-0.0.2 → things.py-0.0.3}/things.py.egg-info/PKG-INFO +2 -2
- {things.py-0.0.2 → things.py-0.0.3}/setup.cfg +0 -0
- {things.py-0.0.2 → things.py-0.0.3}/tests/__init__.py +0 -0
- {things.py-0.0.2 → things.py-0.0.3}/things.py.egg-info/SOURCES.txt +0 -0
- {things.py-0.0.2 → things.py-0.0.3}/things.py.egg-info/dependency_links.txt +0 -0
- {things.py-0.0.2 → things.py-0.0.3}/things.py.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: things.py
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: A simple Python 3 library to read your Things app data.
|
|
5
5
|
Home-page: https://github.com/thingsapi/things.py
|
|
6
6
|
Author: Alexander Willner, Michael B.
|
|
@@ -14,7 +14,7 @@ Description: # Things Python API
|
|
|
14
14
|
[](https://codecov.io/gh/thingsapi/things.py)
|
|
15
15
|
[](https://github.com/thingsapi/things.py/releases)
|
|
16
16
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
17
|
-
[](https://pypi.org/project/things/)
|
|
17
|
+
[](https://pypi.org/project/things.py/)
|
|
18
18
|
[](https://github.com/thingsapi/things.py/releases)
|
|
19
19
|
[](https://github.com/thingsapi/things.py/issues)
|
|
20
20
|
[](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
|
|
@@ -6,7 +6,7 @@ A simple Python 3 library to read your [Things app](https://culturedcode.com/thi
|
|
|
6
6
|
[](https://codecov.io/gh/thingsapi/things.py)
|
|
7
7
|
[](https://github.com/thingsapi/things.py/releases)
|
|
8
8
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
9
|
-
[](https://pypi.org/project/things/)
|
|
9
|
+
[](https://pypi.org/project/things.py/)
|
|
10
10
|
[](https://github.com/thingsapi/things.py/releases)
|
|
11
11
|
[](https://github.com/thingsapi/things.py/issues)
|
|
12
12
|
[](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
|
|
@@ -14,16 +14,16 @@ def package_files(directory):
|
|
|
14
14
|
return paths
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
APP = [
|
|
17
|
+
APP = [""]
|
|
18
18
|
APP_NAME = "Things"
|
|
19
19
|
AUTHOR = "Alexander Willner, Michael B."
|
|
20
20
|
AUTHOR_MAIL = "alex@willner.ws"
|
|
21
21
|
DESCRIPTON = "A simple Python 3 library to read your Things app data."
|
|
22
22
|
URL = "https://github.com/thingsapi/things.py"
|
|
23
|
-
VERSION = "0.0.
|
|
24
|
-
DATA_FILES = package_files(
|
|
23
|
+
VERSION = "0.0.3"
|
|
24
|
+
DATA_FILES = package_files("")
|
|
25
25
|
OPTIONS = {
|
|
26
|
-
|
|
26
|
+
"argv_emulation": False,
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
with open("README.md", "r") as fh:
|
|
@@ -44,11 +44,11 @@ setup(
|
|
|
44
44
|
"Programming Language :: Python :: 3",
|
|
45
45
|
"License :: OSI Approved :: Apache Software License",
|
|
46
46
|
"Operating System :: MacOS :: MacOS X",
|
|
47
|
-
"Natural Language :: English"
|
|
47
|
+
"Natural Language :: English",
|
|
48
48
|
],
|
|
49
|
-
python_requires=
|
|
49
|
+
python_requires=">=3.6",
|
|
50
50
|
version=VERSION,
|
|
51
51
|
data_files=DATA_FILES,
|
|
52
|
-
options={
|
|
53
|
-
setup_requires=[
|
|
52
|
+
options={"py2app": OPTIONS},
|
|
53
|
+
setup_requires=["py2app"],
|
|
54
54
|
)
|
|
@@ -16,9 +16,9 @@ class ThingsCase(unittest.TestCase):
|
|
|
16
16
|
|
|
17
17
|
def test_search(self):
|
|
18
18
|
"""Test search."""
|
|
19
|
-
tasks = things.search(
|
|
19
|
+
tasks = things.search("wrong_query", **FILEPATH)
|
|
20
20
|
self.assertEqual(0, len(tasks))
|
|
21
|
-
tasks = things.search(
|
|
21
|
+
tasks = things.search("To-Do % Heading", **FILEPATH)
|
|
22
22
|
self.assertEqual(1, len(tasks))
|
|
23
23
|
|
|
24
24
|
def test_inbox(self):
|
|
@@ -31,9 +31,9 @@ class ThingsCase(unittest.TestCase):
|
|
|
31
31
|
tasks = things.upcoming(**FILEPATH)
|
|
32
32
|
self.assertEqual(2, len(tasks))
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
"""Test
|
|
36
|
-
tasks = things.
|
|
34
|
+
def test_deadlines(self):
|
|
35
|
+
"""Test deadlines."""
|
|
36
|
+
tasks = things.deadlines(**FILEPATH)
|
|
37
37
|
self.assertEqual(1, len(tasks))
|
|
38
38
|
|
|
39
39
|
def test_today(self):
|
|
@@ -85,11 +85,24 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
|
|
|
85
85
|
If the argument is `True`, only include tasks _with_ a start date.
|
|
86
86
|
If the argument is `None` (default), then include all tasks.
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
If the argument is `False`, only include tasks _without_ a
|
|
90
|
-
If the argument is `True`, only include tasks _with_ a
|
|
88
|
+
deadline : bool or None, optional
|
|
89
|
+
If the argument is `False`, only include tasks _without_ a deadline.
|
|
90
|
+
If the argument is `True`, only include tasks _with_ a deadline.
|
|
91
91
|
If the argument is `None` (default), then include all tasks.
|
|
92
92
|
|
|
93
|
+
search_query : str, optional
|
|
94
|
+
The string value is passed to the SQL LIKE operator. It can thus
|
|
95
|
+
include placeholders such as '%' and '_'. Per default, it is
|
|
96
|
+
wrapped in '% ... %'.
|
|
97
|
+
|
|
98
|
+
Currently titles and notes of to-dos, projects, headings, and areas
|
|
99
|
+
are taken into account.
|
|
100
|
+
|
|
101
|
+
If the query is `False`, then return items where titles or notes
|
|
102
|
+
are empty.
|
|
103
|
+
|
|
104
|
+
If the query `None`, then return all items.
|
|
105
|
+
|
|
93
106
|
index : {'index', 'todayIndex'}, default 'index'
|
|
94
107
|
Database field to order result by.
|
|
95
108
|
|
|
@@ -315,26 +328,35 @@ def tags(title=None, include_items=False, **kwargs):
|
|
|
315
328
|
# Utility API functions derived from above
|
|
316
329
|
# --------------------------------------------------
|
|
317
330
|
|
|
331
|
+
|
|
318
332
|
def search(query: str, **kwargs) -> List[Dict]:
|
|
319
333
|
"""
|
|
320
|
-
Search the database.
|
|
334
|
+
Search tasks in the database.
|
|
321
335
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
query : str
|
|
325
|
-
The string with optional placeholders (%) to search for.
|
|
336
|
+
Currently any part of a title and note of a to-do, project,
|
|
337
|
+
heading, or area is matched.
|
|
326
338
|
|
|
327
|
-
|
|
328
|
-
-------
|
|
329
|
-
list of dict
|
|
330
|
-
Representing Things items.
|
|
339
|
+
See the `search_query` parameter of `api.tasks` for details.
|
|
331
340
|
|
|
332
341
|
Examples
|
|
333
342
|
--------
|
|
334
|
-
>>> things.search('
|
|
335
|
-
|
|
343
|
+
>>> things.search('book')
|
|
344
|
+
[{'uuid': 'YrOmUnEXASmpq8ch6RsyPt',
|
|
345
|
+
'type': 'to-do',
|
|
346
|
+
'title': 'Book a hotel room',
|
|
347
|
+
...},
|
|
348
|
+
{'uuid': 'KVHAxIIJ52a0h1RCbmg5D6',
|
|
349
|
+
'type': 'to-do',
|
|
350
|
+
'title': 'Book flights',
|
|
351
|
+
...}]
|
|
352
|
+
|
|
353
|
+
>>> things.search('book%room')
|
|
354
|
+
[{'uuid': 'YrOmUnEXASmpq8ch6RsyPt',
|
|
355
|
+
'type': 'to-do',
|
|
356
|
+
'title': 'Book a hotel room',
|
|
357
|
+
...}]
|
|
336
358
|
"""
|
|
337
|
-
return tasks(
|
|
359
|
+
return tasks(search_query=query, **kwargs)
|
|
338
360
|
|
|
339
361
|
|
|
340
362
|
def get(uuid, default=None, **kwargs):
|
|
@@ -405,8 +427,8 @@ def today(**kwargs):
|
|
|
405
427
|
|
|
406
428
|
def upcoming(**kwargs):
|
|
407
429
|
"""
|
|
408
|
-
Note: unscheduled tasks with a
|
|
409
|
-
See the `
|
|
430
|
+
Note: unscheduled tasks with a deadline are not included here.
|
|
431
|
+
See the `deadline` function instead.
|
|
410
432
|
"""
|
|
411
433
|
return tasks(start_date=True, start="Someday", **kwargs)
|
|
412
434
|
|
|
@@ -444,9 +466,9 @@ def completed(**kwargs):
|
|
|
444
466
|
return tasks(status="completed", **kwargs)
|
|
445
467
|
|
|
446
468
|
|
|
447
|
-
def
|
|
448
|
-
result = tasks(
|
|
449
|
-
result.sort(key=lambda task: task["
|
|
469
|
+
def deadlines(**kwargs):
|
|
470
|
+
result = tasks(deadline=True, **kwargs)
|
|
471
|
+
result.sort(key=lambda task: task["deadline"])
|
|
450
472
|
return result
|
|
451
473
|
|
|
452
474
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
-
"""Simple read-only API for Things.
|
|
4
|
+
"""Simple read-only API for Things.
|
|
5
|
+
|
|
6
|
+
We attempt to follow the names used in the Things app, URL Scheme,
|
|
7
|
+
and SQL database whenever possible.
|
|
8
|
+
"""
|
|
5
9
|
|
|
6
10
|
from __future__ import print_function
|
|
7
11
|
|
|
@@ -25,19 +29,19 @@ DEFAULT_DATABASE_FILEPATH = os.path.expanduser(
|
|
|
25
29
|
"/Things Database.thingsdatabase/main.sqlite"
|
|
26
30
|
)
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
START_TO_FILTER = {
|
|
29
33
|
"Inbox": "start = 0",
|
|
30
34
|
"Anytime": "start = 1",
|
|
31
35
|
"Someday": "start = 2",
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
STATUS_TO_FILTER = {
|
|
35
39
|
"incomplete": "status = 0",
|
|
36
40
|
"canceled": "status = 2",
|
|
37
41
|
"completed": "status = 3",
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
TYPE_TO_FILTER = {"to-do": "type = 0", "project": "type = 1", "heading": "type = 2"}
|
|
41
45
|
|
|
42
46
|
INDICES = ("index", "todayIndex")
|
|
43
47
|
|
|
@@ -67,25 +71,25 @@ class Database:
|
|
|
67
71
|
TABLE_META = "Meta"
|
|
68
72
|
DATE_CREATE = "creationDate"
|
|
69
73
|
DATE_MOD = "userModificationDate"
|
|
70
|
-
|
|
74
|
+
DATE_DEADLINE = "dueDate"
|
|
71
75
|
DATE_START = "startDate"
|
|
72
76
|
DATE_STOP = "stopDate"
|
|
73
|
-
IS_INBOX =
|
|
74
|
-
IS_ANYTIME =
|
|
75
|
-
IS_SOMEDAY =
|
|
77
|
+
IS_INBOX = START_TO_FILTER["Inbox"]
|
|
78
|
+
IS_ANYTIME = START_TO_FILTER["Anytime"]
|
|
79
|
+
IS_SOMEDAY = START_TO_FILTER["Someday"]
|
|
76
80
|
IS_SCHEDULED = f"{DATE_START} IS NOT NULL"
|
|
77
81
|
IS_NOT_SCHEDULED = f"{DATE_START} IS NULL"
|
|
78
|
-
|
|
82
|
+
IS_DEADLINE = f"{DATE_DEADLINE} IS NOT NULL"
|
|
79
83
|
IS_RECURRING = "recurrenceRule IS NOT NULL"
|
|
80
84
|
IS_NOT_RECURRING = "recurrenceRule IS NULL"
|
|
81
|
-
IS_TODO =
|
|
82
|
-
IS_PROJECT =
|
|
83
|
-
IS_HEADING =
|
|
85
|
+
IS_TODO = TYPE_TO_FILTER["to-do"]
|
|
86
|
+
IS_PROJECT = TYPE_TO_FILTER["project"]
|
|
87
|
+
IS_HEADING = TYPE_TO_FILTER["heading"]
|
|
84
88
|
IS_NOT_TRASHED = "trashed = 0"
|
|
85
89
|
IS_TRASHED = "trashed = 1"
|
|
86
|
-
IS_INCOMPLETE =
|
|
87
|
-
IS_CANCELED =
|
|
88
|
-
IS_COMPLETED =
|
|
90
|
+
IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"]
|
|
91
|
+
IS_CANCELED = STATUS_TO_FILTER["canceled"]
|
|
92
|
+
IS_COMPLETED = STATUS_TO_FILTER["completed"]
|
|
89
93
|
RECURRING_IS_NOT_PAUSED = "instanceCreationPaused = 0"
|
|
90
94
|
RECURRING_HAS_NEXT_STARTDATE = "nextInstanceStartDate IS NOT NULL"
|
|
91
95
|
|
|
@@ -120,10 +124,10 @@ class Database:
|
|
|
120
124
|
heading=None,
|
|
121
125
|
tag=None,
|
|
122
126
|
start_date=None,
|
|
123
|
-
|
|
127
|
+
deadline=None,
|
|
124
128
|
index="index",
|
|
125
129
|
count_only=False,
|
|
126
|
-
|
|
130
|
+
search_query=None,
|
|
127
131
|
):
|
|
128
132
|
"""Get tasks. See `api.tasks` for details on parameters."""
|
|
129
133
|
|
|
@@ -131,9 +135,9 @@ class Database:
|
|
|
131
135
|
start = start and start.title()
|
|
132
136
|
|
|
133
137
|
# Validation
|
|
134
|
-
validate("type", type, [None] + list(
|
|
135
|
-
validate("status", status, [None] + list(
|
|
136
|
-
validate("start", start, [None] + list(
|
|
138
|
+
validate("type", type, [None] + list(TYPE_TO_FILTER))
|
|
139
|
+
validate("status", status, [None] + list(STATUS_TO_FILTER))
|
|
140
|
+
validate("start", start, [None] + list(START_TO_FILTER))
|
|
137
141
|
validate("index", index, list(INDICES))
|
|
138
142
|
|
|
139
143
|
if tag is not None:
|
|
@@ -149,39 +153,39 @@ class Database:
|
|
|
149
153
|
# See: https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute
|
|
150
154
|
|
|
151
155
|
if uuid:
|
|
152
|
-
|
|
156
|
+
where_predicate = f"TRUE {make_filter('TASK.uuid', uuid)}"
|
|
153
157
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
start_filter = START_TO_FILTER.get(start, "")
|
|
159
|
+
status_filter = STATUS_TO_FILTER.get(status, "")
|
|
160
|
+
type_filter = TYPE_TO_FILTER.get(type, "")
|
|
157
161
|
|
|
158
|
-
|
|
162
|
+
where_predicate = f"""
|
|
159
163
|
TASK.{self.IS_NOT_TRASHED}
|
|
160
|
-
{f"AND TASK.{
|
|
161
|
-
{f"AND TASK.{
|
|
162
|
-
{f"AND TASK.{
|
|
164
|
+
{type_filter and f"AND TASK.{type_filter}"}
|
|
165
|
+
{start_filter and f"AND TASK.{start_filter}"}
|
|
166
|
+
{status_filter and f"AND TASK.{status_filter}"}
|
|
163
167
|
AND TASK.{self.IS_NOT_RECURRING}
|
|
164
168
|
AND (PROJECT.title IS NULL OR PROJECT.{self.IS_NOT_TRASHED})
|
|
165
169
|
AND (HEADPROJ.title IS NULL OR HEADPROJ.{self.IS_NOT_TRASHED})
|
|
166
|
-
{make_search(querystr)}
|
|
167
170
|
{make_filter('TASK.uuid', uuid)}
|
|
168
171
|
{make_filter("TASK.area", area)}
|
|
169
172
|
{make_filter("TASK.project", project)}
|
|
170
173
|
{make_filter("TASK.actionGroup", heading)}
|
|
171
174
|
{make_filter("TASK.startDate", start_date)}
|
|
172
|
-
{make_filter("TASK.
|
|
175
|
+
{make_filter(f"TASK.{self.DATE_DEADLINE}", deadline)}
|
|
173
176
|
{make_filter("TAG.title", tag)}
|
|
177
|
+
{make_search_filter(search_query)}
|
|
174
178
|
ORDER BY TASK."{index}"
|
|
175
179
|
"""
|
|
176
180
|
|
|
177
|
-
|
|
181
|
+
sql_query = self.make_task_sql_query(where_predicate)
|
|
178
182
|
if count_only:
|
|
179
|
-
return self.get_count(
|
|
183
|
+
return self.get_count(sql_query)
|
|
180
184
|
else:
|
|
181
|
-
return self.execute_query(
|
|
185
|
+
return self.execute_query(sql_query)
|
|
182
186
|
|
|
183
|
-
def
|
|
184
|
-
"""Make SQL for
|
|
187
|
+
def make_task_sql_query(self, where_predicate):
|
|
188
|
+
"""Make SQL query for Task table"""
|
|
185
189
|
return f"""
|
|
186
190
|
SELECT DISTINCT
|
|
187
191
|
TASK.uuid,
|
|
@@ -221,8 +225,8 @@ class Database:
|
|
|
221
225
|
WHEN CHECKLIST_ITEM.uuid IS NOT NULL THEN 1
|
|
222
226
|
END AS checklist,
|
|
223
227
|
date(TASK.startDate, "unixepoch") AS start_date,
|
|
224
|
-
date(TASK.
|
|
225
|
-
date(TASK.stopDate, "unixepoch") AS stop_date,
|
|
228
|
+
date(TASK.{self.DATE_DEADLINE}, "unixepoch") AS deadline,
|
|
229
|
+
date(TASK.stopDate, "unixepoch") AS "stop_date",
|
|
226
230
|
datetime(TASK.{self.DATE_CREATE}, "unixepoch", "localtime") AS created,
|
|
227
231
|
datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified
|
|
228
232
|
FROM
|
|
@@ -244,11 +248,11 @@ class Database:
|
|
|
244
248
|
ON CHECKLIST_ITEM.task = TASK.uuid
|
|
245
249
|
WHERE
|
|
246
250
|
{self.filter}
|
|
247
|
-
{
|
|
251
|
+
{where_predicate}
|
|
248
252
|
"""
|
|
249
253
|
|
|
250
|
-
def
|
|
251
|
-
return self.execute_query(self.
|
|
254
|
+
def get_task_rows(self, where_predicate):
|
|
255
|
+
return self.execute_query(self.make_task_sql_query(where_predicate))
|
|
252
256
|
|
|
253
257
|
def get_areas(self, uuid=None, tag=None, count_only=False):
|
|
254
258
|
"""Get areas. See `api.areas` for details on parameters."""
|
|
@@ -266,7 +270,7 @@ class Database:
|
|
|
266
270
|
raise ValueError(f"No such area uuid found: {uuid!r}")
|
|
267
271
|
|
|
268
272
|
# Query
|
|
269
|
-
|
|
273
|
+
sql_query = f"""
|
|
270
274
|
SELECT
|
|
271
275
|
AREA.uuid,
|
|
272
276
|
'area' as type,
|
|
@@ -287,13 +291,13 @@ class Database:
|
|
|
287
291
|
ORDER BY AREA."index"
|
|
288
292
|
"""
|
|
289
293
|
if count_only:
|
|
290
|
-
return self.get_count(
|
|
294
|
+
return self.get_count(sql_query)
|
|
291
295
|
else:
|
|
292
|
-
return self.execute_query(
|
|
296
|
+
return self.execute_query(sql_query)
|
|
293
297
|
|
|
294
298
|
def get_checklist_items(self, task_uuid=None):
|
|
295
299
|
"""Get checklist items."""
|
|
296
|
-
|
|
300
|
+
sql_query = f"""
|
|
297
301
|
SELECT
|
|
298
302
|
CHECKLIST_ITEM.title,
|
|
299
303
|
CASE
|
|
@@ -302,7 +306,7 @@ class Database:
|
|
|
302
306
|
WHEN CHECKLIST_ITEM.{self.IS_CANCELED} THEN 'canceled'
|
|
303
307
|
END AS status,
|
|
304
308
|
date(CHECKLIST_ITEM.stopDate, "unixepoch") AS stop_date,
|
|
305
|
-
'checklist
|
|
309
|
+
'checklist-item' as type,
|
|
306
310
|
CHECKLIST_ITEM.uuid,
|
|
307
311
|
datetime(
|
|
308
312
|
CHECKLIST_ITEM.{self.DATE_MOD}, "unixepoch", "localtime"
|
|
@@ -316,7 +320,7 @@ class Database:
|
|
|
316
320
|
CHECKLIST_ITEM.task = ?
|
|
317
321
|
ORDER BY CHECKLIST_ITEM."index"
|
|
318
322
|
"""
|
|
319
|
-
return self.execute_query(
|
|
323
|
+
return self.execute_query(sql_query, (task_uuid,))
|
|
320
324
|
|
|
321
325
|
def get_tags(self, title=None, area=None, task=None, titles_only=False):
|
|
322
326
|
"""Get tags. See `api.tags` for details on parameters."""
|
|
@@ -333,84 +337,83 @@ class Database:
|
|
|
333
337
|
return self.get_tags_of_area(area)
|
|
334
338
|
|
|
335
339
|
if titles_only:
|
|
336
|
-
|
|
337
|
-
return self.execute_query(
|
|
340
|
+
sql_query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
|
|
341
|
+
return self.execute_query(sql_query, row_factory=list_factory)
|
|
338
342
|
else:
|
|
339
|
-
|
|
340
|
-
SELECT
|
|
341
|
-
uuid,
|
|
342
|
-
'tag' AS type,
|
|
343
|
-
title,
|
|
344
|
-
shortcut
|
|
345
|
-
FROM
|
|
346
|
-
{self.TABLE_TAG}
|
|
347
|
-
WHERE
|
|
348
|
-
TRUE
|
|
349
|
-
{make_filter('title', title)}
|
|
350
|
-
ORDER BY "index"
|
|
351
|
-
"""
|
|
352
|
-
return self.execute_query(query)
|
|
353
|
-
|
|
354
|
-
def get_tags_of_task(self, task_uuid):
|
|
355
|
-
"""Get tag titles of task"""
|
|
356
|
-
query = f"""
|
|
343
|
+
sql_query = f"""
|
|
357
344
|
SELECT
|
|
358
|
-
|
|
345
|
+
uuid,
|
|
346
|
+
'tag' AS type,
|
|
347
|
+
title,
|
|
348
|
+
shortcut
|
|
359
349
|
FROM
|
|
360
|
-
{self.
|
|
361
|
-
LEFT OUTER JOIN
|
|
362
|
-
{self.TABLE_TAG} TAG ON TAG.uuid = TASK_TAG.tags
|
|
350
|
+
{self.TABLE_TAG}
|
|
363
351
|
WHERE
|
|
364
|
-
|
|
365
|
-
|
|
352
|
+
TRUE
|
|
353
|
+
{make_filter('title', title)}
|
|
354
|
+
ORDER BY "index"
|
|
366
355
|
"""
|
|
356
|
+
return self.execute_query(sql_query)
|
|
357
|
+
|
|
358
|
+
def get_tags_of_task(self, task_uuid):
|
|
359
|
+
"""Get tag titles of task"""
|
|
360
|
+
sql_query = f"""
|
|
361
|
+
SELECT
|
|
362
|
+
TAG.title
|
|
363
|
+
FROM
|
|
364
|
+
{self.TABLE_TASKTAG} AS TASK_TAG
|
|
365
|
+
LEFT OUTER JOIN
|
|
366
|
+
{self.TABLE_TAG} TAG ON TAG.uuid = TASK_TAG.tags
|
|
367
|
+
WHERE
|
|
368
|
+
TASK_TAG.tasks = ?
|
|
369
|
+
ORDER BY TAG."index"
|
|
370
|
+
"""
|
|
367
371
|
return self.execute_query(
|
|
368
|
-
|
|
372
|
+
sql_query, parameters=(task_uuid,), row_factory=list_factory
|
|
369
373
|
)
|
|
370
374
|
|
|
371
375
|
def get_tags_of_area(self, area_uuid):
|
|
372
376
|
"""Get tag titles for area"""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
377
|
+
sql_query = f"""
|
|
378
|
+
SELECT
|
|
379
|
+
AREA.title
|
|
380
|
+
FROM
|
|
381
|
+
{self.TABLE_AREATAG} AS AREA_TAG
|
|
382
|
+
LEFT OUTER JOIN
|
|
383
|
+
{self.TABLE_TAG} AREA ON AREA.uuid = AREA_TAG.tags
|
|
384
|
+
WHERE
|
|
385
|
+
AREA_TAG.areas = ?
|
|
386
|
+
ORDER BY AREA."index"
|
|
387
|
+
"""
|
|
385
388
|
return self.execute_query(
|
|
386
|
-
|
|
389
|
+
sql_query, parameters=(area_uuid,), row_factory=list_factory
|
|
387
390
|
)
|
|
388
391
|
|
|
389
392
|
def get_version(self):
|
|
390
393
|
"""Get Things Database version."""
|
|
391
394
|
import plistlib
|
|
392
395
|
|
|
393
|
-
|
|
394
|
-
result = self.execute_query(
|
|
396
|
+
sql_query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
|
|
397
|
+
result = self.execute_query(sql_query, row_factory=list_factory)
|
|
395
398
|
plist_bytes = result[0].encode()
|
|
396
399
|
return plistlib.loads(plist_bytes)
|
|
397
400
|
|
|
398
401
|
def get_count(self, sql):
|
|
399
|
-
|
|
400
|
-
return self.execute_query(
|
|
402
|
+
sql_query = f"""SELECT COUNT(uuid) FROM ({sql})"""
|
|
403
|
+
return self.execute_query(sql_query, row_factory=list_factory)[0]
|
|
401
404
|
|
|
402
405
|
# noqa todo: add type hinting for resutl (List[Tuple[str, Any]]?)
|
|
403
|
-
def execute_query(self,
|
|
404
|
-
"""Run the actual query"""
|
|
406
|
+
def execute_query(self, sql_query, parameters=(), row_factory=None):
|
|
407
|
+
"""Run the actual SQL query"""
|
|
405
408
|
if self.debug is True:
|
|
406
409
|
print(self.filepath)
|
|
407
|
-
print(
|
|
410
|
+
print(sql_query)
|
|
408
411
|
try:
|
|
409
412
|
uri = f"file:{self.filepath}?mode=ro" # "ro" means read-only
|
|
410
413
|
connection = sqlite3.connect(uri, uri=True)
|
|
411
414
|
connection.row_factory = row_factory or dict_factory
|
|
412
415
|
cursor = connection.cursor()
|
|
413
|
-
cursor.execute(
|
|
416
|
+
cursor.execute(sql_query, parameters)
|
|
414
417
|
tasks = cursor.fetchall()
|
|
415
418
|
if self.debug:
|
|
416
419
|
for task in tasks:
|
|
@@ -441,7 +444,7 @@ class Database:
|
|
|
441
444
|
TASK.{self.IS_TODO}
|
|
442
445
|
ORDER BY TASK.{self.DATE_STOP}
|
|
443
446
|
"""
|
|
444
|
-
return self.
|
|
447
|
+
return self.get_task_rows(query)
|
|
445
448
|
|
|
446
449
|
def get_lint(self):
|
|
447
450
|
"""Get tasks that float around"""
|
|
@@ -454,7 +457,7 @@ class Database:
|
|
|
454
457
|
TASK.area IS NULL AND
|
|
455
458
|
TASK.actionGroup IS NULL
|
|
456
459
|
"""
|
|
457
|
-
return self.
|
|
460
|
+
return self.get_task_rows(query)
|
|
458
461
|
|
|
459
462
|
def get_empty_projects(self):
|
|
460
463
|
"""Get projects that are empty"""
|
|
@@ -480,7 +483,7 @@ class Database:
|
|
|
480
483
|
)
|
|
481
484
|
) = 0
|
|
482
485
|
"""
|
|
483
|
-
return self.
|
|
486
|
+
return self.get_task_rows(query)
|
|
484
487
|
|
|
485
488
|
def get_largest_projects(self):
|
|
486
489
|
"""Get projects that are empty"""
|
|
@@ -667,33 +670,40 @@ def make_filter(column, value):
|
|
|
667
670
|
}.get(value, default)
|
|
668
671
|
|
|
669
672
|
|
|
670
|
-
def
|
|
671
|
-
|
|
673
|
+
def make_search_filter(query: str) -> str:
|
|
674
|
+
"""
|
|
675
|
+
Example
|
|
676
|
+
-------
|
|
677
|
+
>>> make_search_filter('dinner')
|
|
678
|
+
'AND (
|
|
679
|
+
TASK.title LIKE "%dinner%"
|
|
680
|
+
OR TASK.notes LIKE "%dinner%"
|
|
681
|
+
OR AREA.title LIKE "%dinner%"
|
|
682
|
+
)'
|
|
683
|
+
"""
|
|
684
|
+
if not query:
|
|
685
|
+
return ""
|
|
686
|
+
|
|
672
687
|
# noqa todo 'TMChecklistItem.title'
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if query.endswith(' OR '):
|
|
679
|
-
query = query[:-4]
|
|
680
|
-
if query != "":
|
|
681
|
-
query = 'AND (' + query + ')'
|
|
682
|
-
return query
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
def make_sub_search(column, value):
|
|
686
|
-
default = f'{column} LIKE "%{value}%"'
|
|
687
|
-
return {
|
|
688
|
-
None: "",
|
|
689
|
-
False: f"{column} IS NULL",
|
|
690
|
-
True: f"{column} IS NOT NULL",
|
|
691
|
-
}.get(value, default)
|
|
688
|
+
columns = ["TASK.title", "TASK.notes", "AREA.title"]
|
|
689
|
+
|
|
690
|
+
sub_searches = (f'{column} LIKE "%{query}%"' for column in columns)
|
|
691
|
+
|
|
692
|
+
return f"AND ({' OR '.join(sub_searches)})"
|
|
692
693
|
|
|
693
694
|
|
|
694
695
|
def validate(parameter, argument, valid_arguments):
|
|
695
696
|
"""
|
|
696
697
|
For a given parameter, check if its argument type is valid.
|
|
698
|
+
If not, then raise ValueError.
|
|
699
|
+
|
|
700
|
+
Example
|
|
701
|
+
-------
|
|
702
|
+
>>> validate(
|
|
703
|
+
parameter='status',
|
|
704
|
+
argument='completed',
|
|
705
|
+
valid_arguments=['incomplete', 'completed']
|
|
706
|
+
)
|
|
697
707
|
"""
|
|
698
708
|
if argument in valid_arguments:
|
|
699
709
|
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: things.py
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: A simple Python 3 library to read your Things app data.
|
|
5
5
|
Home-page: https://github.com/thingsapi/things.py
|
|
6
6
|
Author: Alexander Willner, Michael B.
|
|
@@ -14,7 +14,7 @@ Description: # Things Python API
|
|
|
14
14
|
[](https://codecov.io/gh/thingsapi/things.py)
|
|
15
15
|
[](https://github.com/thingsapi/things.py/releases)
|
|
16
16
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
17
|
-
[](https://pypi.org/project/things/)
|
|
17
|
+
[](https://pypi.org/project/things.py/)
|
|
18
18
|
[](https://github.com/thingsapi/things.py/releases)
|
|
19
19
|
[](https://github.com/thingsapi/things.py/issues)
|
|
20
20
|
[](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|