things-py 0.0.1__py3-none-any.whl
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.
- tests/__init__.py +0 -0
- tests/test_things.py +115 -0
- things/__init__.py +23 -0
- things/api.py +485 -0
- things/database.py +702 -0
- things.py-0.0.1.dist-info/LICENSE +201 -0
- things.py-0.0.1.dist-info/METADATA +139 -0
- things.py-0.0.1.dist-info/RECORD +10 -0
- things.py-0.0.1.dist-info/WHEEL +5 -0
- things.py-0.0.1.dist-info/top_level.txt +2 -0
tests/__init__.py
ADDED
|
File without changes
|
tests/test_things.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Module documentation goes here."""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
import things
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FILEPATH = dict(filepath="tests/main.sqlite")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThingsCase(unittest.TestCase):
|
|
15
|
+
"""Class documentation goes here."""
|
|
16
|
+
|
|
17
|
+
def test_search(self):
|
|
18
|
+
"""Test search."""
|
|
19
|
+
tasks = things.search('wrong_query', **FILEPATH)
|
|
20
|
+
self.assertEqual(0, len(tasks))
|
|
21
|
+
tasks = things.search('To-Do % Heading', **FILEPATH)
|
|
22
|
+
self.assertEqual(1, len(tasks))
|
|
23
|
+
|
|
24
|
+
def test_inbox(self):
|
|
25
|
+
"""Test inbox."""
|
|
26
|
+
tasks = things.inbox(**FILEPATH)
|
|
27
|
+
self.assertEqual(1, len(tasks))
|
|
28
|
+
|
|
29
|
+
def test_upcoming(self):
|
|
30
|
+
"""Test upcoming."""
|
|
31
|
+
tasks = things.upcoming(**FILEPATH)
|
|
32
|
+
self.assertEqual(2, len(tasks))
|
|
33
|
+
|
|
34
|
+
def test_due(self):
|
|
35
|
+
"""Test due."""
|
|
36
|
+
tasks = things.due(**FILEPATH)
|
|
37
|
+
self.assertEqual(1, len(tasks))
|
|
38
|
+
|
|
39
|
+
def test_today(self):
|
|
40
|
+
"""Test today."""
|
|
41
|
+
tasks = things.today(**FILEPATH)
|
|
42
|
+
self.assertEqual(1, len(tasks))
|
|
43
|
+
|
|
44
|
+
def test_anytime(self):
|
|
45
|
+
"""Test antime."""
|
|
46
|
+
tasks = things.anytime(**FILEPATH)
|
|
47
|
+
self.assertEqual(8, len(tasks))
|
|
48
|
+
|
|
49
|
+
def test_logbook(self):
|
|
50
|
+
"""Test logbook."""
|
|
51
|
+
tasks = things.logbook(**FILEPATH)
|
|
52
|
+
self.assertEqual(21, len(tasks))
|
|
53
|
+
|
|
54
|
+
def test_canceled(self):
|
|
55
|
+
"""Test canceled."""
|
|
56
|
+
tasks = things.canceled(**FILEPATH)
|
|
57
|
+
self.assertEqual(11, len(tasks))
|
|
58
|
+
|
|
59
|
+
def test_completed(self):
|
|
60
|
+
"""Test completed."""
|
|
61
|
+
tasks = things.completed(**FILEPATH)
|
|
62
|
+
self.assertEqual(10, len(tasks))
|
|
63
|
+
|
|
64
|
+
def test_someday(self):
|
|
65
|
+
"""Test someday."""
|
|
66
|
+
tasks = things.someday(**FILEPATH)
|
|
67
|
+
self.assertEqual(1, len(tasks))
|
|
68
|
+
|
|
69
|
+
def test_get(self):
|
|
70
|
+
"""Test get."""
|
|
71
|
+
tasks = things.get("wrong_uuid", **FILEPATH)
|
|
72
|
+
self.assertEqual(None, tasks)
|
|
73
|
+
tasks = things.get("wrong_uuid", "NOT FOUND", **FILEPATH)
|
|
74
|
+
self.assertEqual("NOT FOUND", tasks)
|
|
75
|
+
tasks = things.get("Qt2AY87x2QDdowSn9HKTt1", **FILEPATH)
|
|
76
|
+
self.assertEqual(4, len(tasks))
|
|
77
|
+
|
|
78
|
+
def test_todos(self):
|
|
79
|
+
"""Test all tasks."""
|
|
80
|
+
tasks = things.todos(start="Anytime", **FILEPATH)
|
|
81
|
+
self.assertEqual(5, len(tasks))
|
|
82
|
+
tasks = things.todos(start="Anytime", status="completed", **FILEPATH)
|
|
83
|
+
self.assertEqual(6, len(tasks))
|
|
84
|
+
tasks = things.todos(status="completed", **FILEPATH)
|
|
85
|
+
self.assertEqual(10, len(tasks))
|
|
86
|
+
tasks = things.todos(include_items=True, **FILEPATH)
|
|
87
|
+
self.assertEqual(9, len(tasks))
|
|
88
|
+
with self.assertRaises(ValueError):
|
|
89
|
+
things.todos(status="wrong_value", **FILEPATH)
|
|
90
|
+
tasks = things.tasks("A2oPvtt4dXoypeoLc8uYzY", **FILEPATH)
|
|
91
|
+
self.assertEqual(13, len(tasks))
|
|
92
|
+
|
|
93
|
+
def test_tags(self):
|
|
94
|
+
"""Test all tags."""
|
|
95
|
+
tags = things.tags(**FILEPATH)
|
|
96
|
+
self.assertEqual(5, len(tags))
|
|
97
|
+
|
|
98
|
+
def test_projects(self):
|
|
99
|
+
"""Test all projects."""
|
|
100
|
+
projects = things.projects(**FILEPATH)
|
|
101
|
+
self.assertEqual(2, len(projects))
|
|
102
|
+
|
|
103
|
+
def test_areas(self):
|
|
104
|
+
"""Test all test_areas."""
|
|
105
|
+
test_areas = things.areas(**FILEPATH)
|
|
106
|
+
self.assertEqual(1, len(test_areas))
|
|
107
|
+
|
|
108
|
+
def test_database_version(self):
|
|
109
|
+
"""Test database version."""
|
|
110
|
+
version = things.Database(**FILEPATH).get_version()
|
|
111
|
+
self.assertEqual(18, version)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main()
|
things/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""A simple Python 3 library to read your Things app data."""
|
|
2
|
+
|
|
3
|
+
from things.api import ( # noqa
|
|
4
|
+
anytime,
|
|
5
|
+
areas,
|
|
6
|
+
canceled,
|
|
7
|
+
completed,
|
|
8
|
+
due,
|
|
9
|
+
get,
|
|
10
|
+
inbox,
|
|
11
|
+
link,
|
|
12
|
+
logbook,
|
|
13
|
+
projects,
|
|
14
|
+
search,
|
|
15
|
+
show,
|
|
16
|
+
someday,
|
|
17
|
+
tags,
|
|
18
|
+
tasks,
|
|
19
|
+
today,
|
|
20
|
+
todos,
|
|
21
|
+
upcoming,
|
|
22
|
+
)
|
|
23
|
+
from things.database import Database # noqa
|
things/api.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Module implementing Things API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from shlex import quote
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Dict
|
|
11
|
+
|
|
12
|
+
from .database import Database
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------
|
|
16
|
+
# Core functions
|
|
17
|
+
# --------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
|
|
21
|
+
"""
|
|
22
|
+
Read tasks into dicts.
|
|
23
|
+
|
|
24
|
+
Note: "task" is a technical term used in the database to refer to a
|
|
25
|
+
to-do, project, or heading. For details, check the "type"-parameter.
|
|
26
|
+
|
|
27
|
+
Per default, only tasks marked as incomplete are included. If you
|
|
28
|
+
want to include completed or canceled tasks in the result, check the
|
|
29
|
+
"status"-parameter.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
uuid : str or None, optional
|
|
34
|
+
Any valid task uuid. If None, then return all tasks matched.
|
|
35
|
+
|
|
36
|
+
include_items : boolean, default False
|
|
37
|
+
Include items contained within a task. These might include
|
|
38
|
+
checklist items, headings, and to-dos.
|
|
39
|
+
|
|
40
|
+
type : {'to-do', 'heading', 'project', None}, optional
|
|
41
|
+
Only return a specific type of task:
|
|
42
|
+
|
|
43
|
+
'to-do': may have a checklist; may be in an area and have tags.
|
|
44
|
+
'project': may have to-dos and headings; may be in an area and
|
|
45
|
+
have tags.
|
|
46
|
+
'heading': part of a project; groups tasks.
|
|
47
|
+
None: return all types of tasks.
|
|
48
|
+
|
|
49
|
+
status : {'incomplete', 'completed', 'canceled', None}, optional, \
|
|
50
|
+
default 'incomplete'
|
|
51
|
+
|
|
52
|
+
Only include tasks matching that status. If the argument is `None`,
|
|
53
|
+
then include tasks with any status value.
|
|
54
|
+
|
|
55
|
+
start : {'Inbox', 'Anytime', 'Someday', None}, optional
|
|
56
|
+
Only include tasks matching that start value. If the argument is
|
|
57
|
+
`None` (default), then include tasks with any start value.
|
|
58
|
+
|
|
59
|
+
area : str, bool, or None, optional
|
|
60
|
+
Any valid uuid of an area. Only include tasks matching that area.
|
|
61
|
+
If the argument is `False`, only include tasks _without_ an area.
|
|
62
|
+
If the argument is `True`, only include tasks _with_ an area.
|
|
63
|
+
If the argument is `None` (default), then include all tasks.
|
|
64
|
+
|
|
65
|
+
project : str or None, optional
|
|
66
|
+
Any valid uuid of a project. Only include tasks matching that project.
|
|
67
|
+
If the argument is `False`, only include tasks _without_ a project.
|
|
68
|
+
If the argument is `True`, only include tasks _with_ a project.
|
|
69
|
+
If the argument is `None` (default), then include all tasks.
|
|
70
|
+
|
|
71
|
+
heading : str or None, optional
|
|
72
|
+
Any valid uuid of a heading. Only include tasks matching that heading.
|
|
73
|
+
If the argument is `False`, only include tasks _without_ a heading.
|
|
74
|
+
If the argument is `True`, only include tasks _with_ a heading.
|
|
75
|
+
If the argument is `None` (default), then include all tasks.
|
|
76
|
+
|
|
77
|
+
tag : str or None, optional
|
|
78
|
+
Any valid title of a tag. Only include tasks matching that tag.
|
|
79
|
+
If the argument is `False`, only include tasks _without_ tags.
|
|
80
|
+
If the argument is `True`, only include tasks _with_ tags.
|
|
81
|
+
If the argument is `None` (default), then include all tasks.
|
|
82
|
+
|
|
83
|
+
start_date : bool or None, optional
|
|
84
|
+
If the argument is `False`, only include tasks _without_ a start date.
|
|
85
|
+
If the argument is `True`, only include tasks _with_ a start date.
|
|
86
|
+
If the argument is `None` (default), then include all tasks.
|
|
87
|
+
|
|
88
|
+
due_date : bool or None, optional
|
|
89
|
+
If the argument is `False`, only include tasks _without_ a due date.
|
|
90
|
+
If the argument is `True`, only include tasks _with_ a due date.
|
|
91
|
+
If the argument is `None` (default), then include all tasks.
|
|
92
|
+
|
|
93
|
+
index : {'index', 'todayIndex'}, default 'index'
|
|
94
|
+
Database field to order result by.
|
|
95
|
+
|
|
96
|
+
count_only : boolean, default False
|
|
97
|
+
Only output length of result. This is done by a SQL COUNT query.
|
|
98
|
+
|
|
99
|
+
filepath : str, optional
|
|
100
|
+
Any valid path of a SQLite database file generated by the Things app.
|
|
101
|
+
If no path is provided, then access the default database path.
|
|
102
|
+
|
|
103
|
+
database : things.Database, optional
|
|
104
|
+
Any valid `things.Database` object previously instantiated.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
list of dict (default)
|
|
109
|
+
Representing multiple tasks.
|
|
110
|
+
dict (if `uuid` is given)
|
|
111
|
+
Representing a single task.
|
|
112
|
+
int (count_only == True)
|
|
113
|
+
Count of matching Tasks.
|
|
114
|
+
|
|
115
|
+
Examples
|
|
116
|
+
--------
|
|
117
|
+
>>> things.tasks()
|
|
118
|
+
...
|
|
119
|
+
>>> things.tasks('2Ukg8I2nLukhyEM7wYiBeb')
|
|
120
|
+
...
|
|
121
|
+
>>> things.tasks(area='hIo1FJlAYGKt1Yj38vzKc3', include_items=True)
|
|
122
|
+
...
|
|
123
|
+
>>> things.tasks(status='completed', count_only=True)
|
|
124
|
+
10
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
database = pop_database(kwargs)
|
|
128
|
+
result = database.get_tasks(
|
|
129
|
+
uuid=uuid, status=kwargs.pop("status", "incomplete"), **kwargs
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if kwargs.get("count_only"):
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
# overwrite `include_items` if fetching a single task.
|
|
136
|
+
if uuid:
|
|
137
|
+
include_items = True
|
|
138
|
+
|
|
139
|
+
for task in result:
|
|
140
|
+
# TK: How costly of an operation is it to do this for every task?
|
|
141
|
+
# IF costly, then can it be made significantly more efficient
|
|
142
|
+
# by optimizing SQL calls?
|
|
143
|
+
|
|
144
|
+
if task.get("tags"):
|
|
145
|
+
task["tags"] = database.get_tags(task=task["uuid"])
|
|
146
|
+
|
|
147
|
+
if not include_items:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# include items
|
|
151
|
+
if task["type"] == "to-do":
|
|
152
|
+
if task.get("checklist"):
|
|
153
|
+
task["checklist"] = database.get_checklist_items(task_uuid=task["uuid"])
|
|
154
|
+
elif task["type"] == "project":
|
|
155
|
+
project = task
|
|
156
|
+
project["items"] = items = tasks(
|
|
157
|
+
project=project["uuid"],
|
|
158
|
+
include_items=True,
|
|
159
|
+
database=database,
|
|
160
|
+
)
|
|
161
|
+
# to-dos without headings appear before headings in app
|
|
162
|
+
items.sort(key=lambda item: item["type"], reverse=True)
|
|
163
|
+
elif task["type"] == "heading":
|
|
164
|
+
heading = task
|
|
165
|
+
heading["items"] = tasks(
|
|
166
|
+
type="to-do",
|
|
167
|
+
heading=heading["uuid"],
|
|
168
|
+
include_items=True,
|
|
169
|
+
database=database,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if uuid:
|
|
173
|
+
result = result[0]
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def areas(uuid=None, include_items=False, **kwargs):
|
|
179
|
+
"""
|
|
180
|
+
Read areas into dicts.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
uuid : str or None, optional
|
|
185
|
+
Any valid uuid of an area. If None, then return all areas.
|
|
186
|
+
|
|
187
|
+
include_items : boolean, default False
|
|
188
|
+
Include tasks and projects in each area.
|
|
189
|
+
|
|
190
|
+
tag : str or None, optional
|
|
191
|
+
Any valid title of a tag. Only include areas matching that tag.
|
|
192
|
+
If the argument is `False`, only include areas _without_ tags.
|
|
193
|
+
If the argument is `True`, only include areas _with_ tags.
|
|
194
|
+
If the argument is `None`, then ignore any tags present, that is,
|
|
195
|
+
include areas both with and without tags.
|
|
196
|
+
|
|
197
|
+
count_only : boolean, default False
|
|
198
|
+
Only output length of result. This is done by a SQL COUNT query.
|
|
199
|
+
|
|
200
|
+
filepath : str, optional
|
|
201
|
+
Any valid path of a SQLite database file generated by the Things app.
|
|
202
|
+
If no path is provided, then access the default database path.
|
|
203
|
+
|
|
204
|
+
database : things.Database, optional
|
|
205
|
+
Any valid `things.Database` object previously instantiated.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
list of dict (default)
|
|
210
|
+
Representing Things areas.
|
|
211
|
+
dict (if `uuid` is given)
|
|
212
|
+
Representing a single Things area.
|
|
213
|
+
int (count_only == True)
|
|
214
|
+
Count of matching areas.
|
|
215
|
+
|
|
216
|
+
Examples
|
|
217
|
+
--------
|
|
218
|
+
>>> things.areas()
|
|
219
|
+
...
|
|
220
|
+
>>> things.areas(tag='Home')
|
|
221
|
+
...
|
|
222
|
+
>>> things.areas(uuid='Gw9QefIdgR6nPEoY5hBNSh')
|
|
223
|
+
...
|
|
224
|
+
>>> things.areas(include_items=True, status='completed')
|
|
225
|
+
...
|
|
226
|
+
"""
|
|
227
|
+
database = pop_database(kwargs)
|
|
228
|
+
result = database.get_areas(uuid=uuid, **kwargs)
|
|
229
|
+
|
|
230
|
+
if kwargs.get("count_only"):
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
for area in result:
|
|
234
|
+
if area.get("tags"):
|
|
235
|
+
area["tags"] = database.get_tags(area=area["uuid"])
|
|
236
|
+
if include_items:
|
|
237
|
+
area["items"] = tasks(
|
|
238
|
+
area=area["uuid"], include_items=True, database=database
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if uuid:
|
|
242
|
+
result = result[0]
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def tags(title=None, include_items=False, **kwargs):
|
|
248
|
+
"""
|
|
249
|
+
Read tags into dicts.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
title : str, optional
|
|
254
|
+
Any valid title of a tag. Include all items of said tag.
|
|
255
|
+
If None, then return all tags.
|
|
256
|
+
|
|
257
|
+
include_items : boolean, default False
|
|
258
|
+
For each tag, include items tagged with that tag.
|
|
259
|
+
Items may include areas, tasks, and projects.
|
|
260
|
+
|
|
261
|
+
area : str, optional
|
|
262
|
+
Valid uuid of an area. Return tags of said area.
|
|
263
|
+
|
|
264
|
+
task : str, optional
|
|
265
|
+
Valid uuid of a task. Return tags of said task.
|
|
266
|
+
|
|
267
|
+
titles_only : bool, default False
|
|
268
|
+
If True, only return list of titles of tags.
|
|
269
|
+
|
|
270
|
+
filepath : str, optional
|
|
271
|
+
Any valid path of a SQLite database file generated by the Things app.
|
|
272
|
+
If no path is provided, then access the default database path.
|
|
273
|
+
|
|
274
|
+
database : things.Database, optional
|
|
275
|
+
Any valid `things.Database` object previously instantiated.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
list of dict (default)
|
|
280
|
+
Representing tags.
|
|
281
|
+
list of str (if `titles_only == True` or area / task is given)
|
|
282
|
+
Representing tag titles.
|
|
283
|
+
dict (if `title` is given)
|
|
284
|
+
Representing a single Things tag.
|
|
285
|
+
|
|
286
|
+
Examples
|
|
287
|
+
--------
|
|
288
|
+
>>> things.tags()
|
|
289
|
+
...
|
|
290
|
+
>>> things.tags('Home')
|
|
291
|
+
...
|
|
292
|
+
>>> things.tags(include_items=True)
|
|
293
|
+
...
|
|
294
|
+
>>> things.tags(task='2Ukg8I2nLukhyEM7wYiBeb')
|
|
295
|
+
...
|
|
296
|
+
"""
|
|
297
|
+
database = pop_database(kwargs)
|
|
298
|
+
result = database.get_tags(title=title, **kwargs)
|
|
299
|
+
|
|
300
|
+
if include_items:
|
|
301
|
+
for tag in result:
|
|
302
|
+
tag_title = tag["title"]
|
|
303
|
+
tag["items"] = [
|
|
304
|
+
*areas(tag=tag_title, database=database),
|
|
305
|
+
*tasks(tag=tag_title, database=database),
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
if title:
|
|
309
|
+
result = result[0]
|
|
310
|
+
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --------------------------------------------------
|
|
315
|
+
# Utility API functions derived from above
|
|
316
|
+
# --------------------------------------------------
|
|
317
|
+
|
|
318
|
+
def search(query: str, **kwargs) -> List[Dict]:
|
|
319
|
+
"""
|
|
320
|
+
Search the database. This takes mainly notes and titles into account.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
query : str
|
|
325
|
+
The string with optional placeholders (%) to search for.
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
list of dict
|
|
330
|
+
Representing Things items.
|
|
331
|
+
|
|
332
|
+
Examples
|
|
333
|
+
--------
|
|
334
|
+
>>> things.search('substring % within notes and titles')
|
|
335
|
+
...
|
|
336
|
+
"""
|
|
337
|
+
return tasks(querystr=query, **kwargs)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get(uuid, default=None, **kwargs):
|
|
341
|
+
"""
|
|
342
|
+
Find an object by uuid. If not found, return `default`.
|
|
343
|
+
|
|
344
|
+
Currently supports tasks, projects, headings, areas, and tags.
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
return tasks(uuid=uuid, **kwargs)
|
|
348
|
+
except ValueError:
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
return areas(uuid=uuid, **kwargs)
|
|
353
|
+
except ValueError:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
for tag in tags(**kwargs):
|
|
357
|
+
if tag["uuid"] == uuid:
|
|
358
|
+
return tag
|
|
359
|
+
|
|
360
|
+
return default
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Filter by object type
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def todos(uuid=None, **kwargs):
|
|
367
|
+
return tasks(uuid=uuid, type="to-do", **kwargs)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def projects(uuid=None, **kwargs):
|
|
371
|
+
return tasks(uuid=uuid, type="project", **kwargs)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Filter by collections in the Things app sidebar.
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def inbox(**kwargs):
|
|
378
|
+
return tasks(start="Inbox", **kwargs)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def today(**kwargs):
|
|
382
|
+
"""
|
|
383
|
+
Note: This might not produce desired results if the Things app hasn't
|
|
384
|
+
been opened yet today. In general, you can assume that whatever state
|
|
385
|
+
the Things app was in when you last opened it, that's the state
|
|
386
|
+
reflected here by the API.
|
|
387
|
+
"""
|
|
388
|
+
database = pop_database(kwargs)
|
|
389
|
+
if not database.was_modified_today():
|
|
390
|
+
print(
|
|
391
|
+
"[NOTE] The results reflect the state of the Things app "
|
|
392
|
+
"when it was last run. If the results seem out of date, "
|
|
393
|
+
"then run the Things app to update the database.",
|
|
394
|
+
file=sys.stderr,
|
|
395
|
+
)
|
|
396
|
+
return tasks(
|
|
397
|
+
start_date=True,
|
|
398
|
+
start="Anytime",
|
|
399
|
+
index="todayIndex",
|
|
400
|
+
database=database,
|
|
401
|
+
**kwargs,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def upcoming(**kwargs):
|
|
406
|
+
"""
|
|
407
|
+
Note: unscheduled tasks with a due date are not included here.
|
|
408
|
+
See the `due` function instead.
|
|
409
|
+
"""
|
|
410
|
+
return tasks(start_date=True, start="Someday", **kwargs)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def anytime(**kwargs):
|
|
414
|
+
return tasks(start="Anytime", **kwargs)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def someday(**kwargs):
|
|
418
|
+
return tasks(start_date=False, start="Someday", **kwargs)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def logbook(**kwargs):
|
|
422
|
+
result = [*canceled(**kwargs), *completed(**kwargs)]
|
|
423
|
+
result.sort(key=lambda task: task["stop_date"], reverse=True)
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# Filter by various task properties
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def canceled(**kwargs):
|
|
431
|
+
return tasks(status="canceled", **kwargs)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def completed(**kwargs):
|
|
435
|
+
"""
|
|
436
|
+
Examples
|
|
437
|
+
--------
|
|
438
|
+
>>> things.completed(count_only=True)
|
|
439
|
+
14
|
|
440
|
+
>>> things.completed(type='project', count_only=True)
|
|
441
|
+
4
|
|
442
|
+
"""
|
|
443
|
+
return tasks(status="completed", **kwargs)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def due(**kwargs):
|
|
447
|
+
result = tasks(due_date=True, **kwargs)
|
|
448
|
+
result.sort(key=lambda task: task["due_date"])
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# Interact with Things app
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def link(uuid):
|
|
456
|
+
return f"things:///show?id={uuid}"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def show(uuid):
|
|
460
|
+
"""
|
|
461
|
+
Show a certain uuid in the Things app.
|
|
462
|
+
|
|
463
|
+
Parameters
|
|
464
|
+
----------
|
|
465
|
+
uuid : str
|
|
466
|
+
A valid uuid of any Things object.
|
|
467
|
+
|
|
468
|
+
Examples
|
|
469
|
+
--------
|
|
470
|
+
>>> tag = things.tags('Home')
|
|
471
|
+
>>> things.show(tag['uuid'])
|
|
472
|
+
"""
|
|
473
|
+
uri = link(uuid)
|
|
474
|
+
os.system(f"open {quote(uri)}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# Helper functions
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def pop_database(kwargs):
|
|
481
|
+
"""instantiate non-default database from `kwargs` if provided"""
|
|
482
|
+
filepath, database = kwargs.pop("filepath", None), kwargs.pop("database", None)
|
|
483
|
+
if not database:
|
|
484
|
+
database = Database(filepath=filepath)
|
|
485
|
+
return database
|