Skip to content

Commit c395bc6

Browse files
version 2.6.3, added OPML support, updated database location, updated requirements, added anonymization support
1 parent 6f74516 commit c395bc6

File tree

10 files changed

+134
-13
lines changed

10 files changed

+134
-13
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-include appstore/Makefile
22

3-
VERSION=2.6.2
3+
VERSION=2.6.3
44
MAIN=things3_kanban
55
APP=things3_app
66
SERVER=things3_api

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ While everything should work out of the box, you might want to change some confi
3131

3232
```ini
3333
[DATABASE]
34-
THINGSDB=/Users/myname/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/Things.sqlite3
34+
THINGSDB=/Users/myname/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/Things Database.thingsdatabase/main.sqlite
3535
TAG_WAITING=Waiting
3636
TAG_MIT=MIT
3737
TAG_CLEANUP=Cleanup

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ flake8==3.7.9
77
python-coveralls==2.9.3
88
flask==1.1.2
99
argcomplete==1.11.1
10+
pywebview==3.3.5
11+
setuptools==45.0.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def package_files(directory):
2222
AUTHOR_MAIL = "[email protected]"
2323
DESCRIPTON = "A simple read-only CLI, API and Web Service for Things 3"
2424
URL = "https://kanbanview.app"
25-
VERSION = "2.6.2"
25+
VERSION = "2.6.3"
2626
DATA_FILES = package_files('resources')
2727
OPTIONS = {
2828
'argv_emulation': False,

things3/things3.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__copyright__ = "2020 Alexander Willner"
1010
__credits__ = ["Alexander Willner"]
1111
__license__ = "Apache License 2.0"
12-
__version__ = "2.6.2"
12+
__version__ = "2.6.3"
1313
__maintainer__ = "Alexander Willner"
1414
__email__ = "[email protected]"
1515
__status__ = "Development"
@@ -30,7 +30,8 @@ class Things3():
3030
# Database info
3131
FILE_CONFIG = str(Path.home()) + '/.kanbanviewrc'
3232
FILE_DB = '/Library/Group Containers/'\
33-
'JLMPQHK86H.com.culturedcode.ThingsMac/Things.sqlite3'
33+
'JLMPQHK86H.com.culturedcode.ThingsMac/'\
34+
'Things Database.thingsdatabase/main.sqlite'
3435
TABLE_TASK = "TMTask"
3536
TABLE_AREA = "TMArea"
3637
TABLE_TAG = "TMTag"
@@ -129,7 +130,7 @@ def __init__(self,
129130

130131
cfg = self.get_from_config(database, 'THINGSDB')
131132
self.database = cfg if cfg else self.database
132-
# Automated migration to new database location in Things 3.12.6
133+
# Automated migration to new database location in Things 3.12.6/3.13.1
133134
# --------------------------------
134135
try:
135136
with open(self.database) as f_d:
@@ -190,7 +191,8 @@ def anonymize_tasks(self, tasks):
190191
if self.anonymize:
191192
for task in tasks:
192193
task['title'] = self.anonymize_string(task['title'])
193-
task['context'] = self.anonymize_string(task['context'])
194+
task['context'] = self.anonymize_string(
195+
task['context']) if 'context' in task else ''
194196
return tasks
195197

196198
def get_inbox(self):
@@ -230,6 +232,34 @@ def get_today(self):
230232
"""
231233
return self.get_rows(query)
232234

235+
def get_task(self, area=None, project=None):
236+
"""Get tasks."""
237+
afilter = f'AND TASK.area = "{area}"' \
238+
if area is not None else ''
239+
pfilter = f'AND TASK.project = "{project}"' \
240+
if project is not None else ''
241+
query = f"""
242+
TASK.{self.IS_NOT_TRASHED} AND
243+
TASK.{self.IS_TASK} AND
244+
TASK.{self.IS_OPEN} AND
245+
TASK.{self.IS_ANYTIME} AND
246+
TASK.{self.IS_NOT_RECURRING} AND (
247+
(
248+
PROJECT.title IS NULL OR (
249+
PROJECT.{self.IS_NOT_TRASHED}
250+
)
251+
) AND (
252+
HEADPROJ.title IS NULL OR (
253+
HEADPROJ.{self.IS_NOT_TRASHED}
254+
)
255+
)
256+
)
257+
{afilter}
258+
{pfilter}
259+
ORDER BY TASK.duedate DESC, TASK.{self.DATE_CREATE} DESC
260+
"""
261+
return self.get_rows(query)
262+
233263
def get_someday(self):
234264
"""Get someday tasks."""
235265
query = f"""
@@ -412,8 +442,9 @@ def get_trashed(self):
412442
"""
413443
return self.get_rows(query)
414444

415-
def get_projects(self):
445+
def get_projects(self, area=None):
416446
"""Get projects."""
447+
afilter = f'AND TASK.area = "{area}"' if area is not None else ''
417448
query = f"""
418449
SELECT
419450
TASK.uuid,
@@ -432,6 +463,7 @@ def get_projects(self):
432463
TASK.{self.IS_NOT_TRASHED} AND
433464
TASK.{self.IS_PROJECT} AND
434465
TASK.{self.IS_OPEN}
466+
{afilter}
435467
ORDER BY TASK.title COLLATE NOCASE
436468
"""
437469
return self.execute_query(query)

things3/things3_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__copyright__ = "Copyright 2020 Alexander Willner"
1010
__credits__ = ["Alexander Willner"]
1111
__license__ = "Apache License 2.0"
12-
__version__ = "2.6.2"
12+
__version__ = "2.6.3"
1313
__maintainer__ = "Alexander Willner"
1414
__email__ = "[email protected]"
1515
__status__ = "Development"

things3/things3_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
__copyright__ = "Copyright 2020 Alexander Willner"
1111
__credits__ = ["Luc Beaulieu", "Alexander Willner"]
1212
__license__ = "Apache License 2.0"
13-
__version__ = "2.6.2"
13+
__version__ = "2.6.3"
1414
__maintainer__ = "Alexander Willner"
1515
__email__ = "[email protected]"
1616
__status__ = "Development"

things3/things3_cli.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__copyright__ = "2020 Alexander Willner"
1010
__credits__ = ["Alexander Willner"]
1111
__license__ = "Apache License 2.0"
12-
__version__ = "2.6.2"
12+
__version__ = "2.6.3"
1313
__maintainer__ = "Alexander Willner"
1414
__email__ = "[email protected]"
1515
__status__ = "Development"
@@ -21,13 +21,16 @@
2121
import webbrowser
2222
import argcomplete # type: ignore
2323
from things3.things3 import Things3
24+
from things3.things3_opml import Things3OPML
2425

2526

2627
class Things3CLI():
2728
"""Simple read-only Thing 3 CLI."""
2829

2930
print_json = False
3031
print_csv = False
32+
print_opml = False
33+
anonymize = False
3134
things3 = None
3235

3336
def __init__(self, database=None):
@@ -37,6 +40,8 @@ def print_tasks(self, tasks):
3740
"""Print a task."""
3841
if self.print_json:
3942
print(json.dumps(tasks))
43+
elif self.print_opml:
44+
Things3OPML().print_tasks(tasks)
4045
elif self.print_csv:
4146
fieldnames = ['uuid', 'title', 'context', 'context_uuid', 'size',
4247
'type', 'due', 'created', 'modified', 'started',
@@ -48,7 +53,7 @@ def print_tasks(self, tasks):
4853
else:
4954
for task in tasks:
5055
title = task['title']
51-
context = task['context']
56+
context = task['context'] if 'context' in task else ''
5257
print(' - ', title, ' (', context, ')')
5358

5459
@classmethod
@@ -88,6 +93,10 @@ def get_parser(cls):
8893
help='Shows all tasks')
8994
subparsers.add_parser('csv',
9095
help='Exports tasks as CSV')
96+
subparsers.add_parser('areas',
97+
help='Shows all areas')
98+
subparsers.add_parser('opml',
99+
help='Exports tasks as OPML')
91100
subparsers.add_parser('due',
92101
help='Shows tasks with due dates')
93102
subparsers.add_parser('empty',
@@ -147,6 +156,14 @@ def get_parser(cls):
147156
action="store_true", default=False,
148157
help="output as CSV", dest="csv")
149158

159+
parser.add_argument("-o", "--opml",
160+
action="store_true", default=False,
161+
help="output as OPML", dest="opml")
162+
163+
parser.add_argument("-a", "--anonymize",
164+
action="store_true", default=False,
165+
help="anonymize output", dest="anonymize")
166+
150167
parser.add_argument(
151168
"--version",
152169
action="version",
@@ -165,10 +182,15 @@ def main(self, args=None):
165182
command = args.command
166183
self.print_json = args.json
167184
self.print_csv = args.csv
185+
self.print_opml = args.opml
186+
self.anonymize = args.anonymize
187+
self.things3.anonymize = self.anonymize
168188

169189
if command in self.things3.functions:
170190
func = self.things3.functions[command]
171191
self.print_tasks(func(self.things3))
192+
elif command == "opml":
193+
Things3OPML().print_all(self.things3)
172194
elif command == "csv":
173195
print("Deprecated: use --csv instead")
174196
elif command == "feedback":

things3/things3_kanban.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__copyright__ = "Copyright 2020 Alexander Willner"
1010
__credits__ = ["Luc Beaulieu", "Alexander Willner"]
1111
__license__ = "Apache License 2.0"
12-
__version__ = "2.6.2"
12+
__version__ = "2.6.3"
1313
__maintainer__ = "Alexander Willner"
1414
__email__ = "[email protected]"
1515
__status__ = "Development"

things3/things3_opml.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""OPML Plugin for the Thing 3 CLI."""
5+
6+
from __future__ import print_function
7+
8+
__author__ = "Alexander Willner"
9+
__copyright__ = "2020 Alexander Willner"
10+
__credits__ = ["Alexander Willner"]
11+
__license__ = "Apache License 2.0"
12+
__version__ = "2.6.3"
13+
__maintainer__ = "Alexander Willner"
14+
__email__ = "[email protected]"
15+
__status__ = "Development"
16+
17+
import xml.etree.ElementTree as ET
18+
from xml.etree.ElementTree import Element, SubElement
19+
from xml.dom import minidom
20+
21+
22+
class Things3OPML():
23+
"""OPML Plugin for Thing 3 CLI."""
24+
25+
@staticmethod
26+
def get_top():
27+
"""Get first element."""
28+
top = Element('opml')
29+
head = SubElement(top, 'head')
30+
title = SubElement(head, 'title')
31+
title.text = 'Things 3 Database'
32+
return top
33+
34+
@staticmethod
35+
def print(top):
36+
"""Print pretty XML."""
37+
xmlstr = minidom.parseString(
38+
ET.tostring(top)).toprettyxml(indent=" ")
39+
print(xmlstr)
40+
41+
def print_tasks(self, tasks):
42+
"""Print pretty XML of selected tasks."""
43+
top = self.get_top()
44+
body = SubElement(top, 'body')
45+
for task in tasks:
46+
SubElement(body, 'outline').set('text', task['title'])
47+
self.print(top)
48+
49+
def print_all(self, things3):
50+
"""Print."""
51+
top = self.get_top()
52+
body = SubElement(top, 'body')
53+
54+
for area in things3.get_areas():
55+
area_element = SubElement(body, 'outline')
56+
area_element.set('text', area['title'])
57+
for task in things3.get_task(area['uuid']):
58+
SubElement(area_element, 'outline').set('text', task['title'])
59+
for project in things3.get_projects(area['uuid']):
60+
project_element = SubElement(area_element, 'outline')
61+
project_element.set('text', project['title'])
62+
for task in things3.get_task(None, project['uuid']):
63+
SubElement(project_element, 'outline').set(
64+
'text', task['title'])
65+
self.print(top)

0 commit comments

Comments
 (0)