Passed
Push — master ( bca70c...674162 )
by Alexander
01:11
created

things_cli.cli.ThingsCLI.print_tasks()   A

Complexity

Conditions 4

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nop 2
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""A simple Python 3 CLI to read your Things app data."""
5
6
from __future__ import print_function
7
8
import argparse
9
import csv
10
from io import StringIO
11
import json
12
import sys
13
import webbrowser
14
from xml.dom import minidom
15
import xml.etree.ElementTree as ETree
16
from xml.etree.ElementTree import Element, SubElement
17
18
import argcomplete  # type: ignore
0 ignored issues
show
introduced by
Unable to import 'argcomplete'
Loading history...
19
import things as api
0 ignored issues
show
introduced by
Unable to import 'things'
Loading history...
20
21
from things_cli import __version__
22
23
24
class ThingsCLI:  # pylint: disable=R0902
25
    """A simple Python 3 CLI to read your Things app data."""
26
27
    print_json = False
28
    print_csv = False
29
    print_opml = False
30
    # anonymize = False
31
    database = None
32
    recursive = False
33
    filter_project = None
34
    filter_area = None
35
    filter_tag = None
36
37
    def __init__(self, database=None):
38
        self.database = database
39
40
    def print_tasks(self, tasks):
41
        """Print a task."""
42
        if self.print_json:
43
            print(json.dumps(tasks))
44
        elif self.print_opml:
45
            print(self.opml_dumps(tasks))
46
        elif self.print_csv:
47
            print(self.csv_dumps(tasks))
48
        else:
49
            print(self.txt_dumps(tasks), end="")
50
51
    def csv_dumps(self, tasks):
52
        """Convert tasks into CSV."""
53
54
        fieldnames = []
55
        self.csv_header(tasks, fieldnames)
56
        if "items" in fieldnames:
57
            fieldnames.remove("items")
58
        if "checklist" in fieldnames:
59
            fieldnames.remove("checklist")
60
61
        output = StringIO()
62
        writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=";")
63
        writer.writeheader()
64
65
        self.csv_converter(tasks, writer)
66
67
        return output.getvalue()
68
69
    def csv_header(self, tasks, fieldnames):
70
        """Convert tasks into CSV header."""
71
        for task in tasks:
72
            fieldnames.extend(field for field in task if field not in fieldnames)
73
            self.csv_header(task.get("items", []), fieldnames)
74
75
    def csv_converter(self, tasks, writer):
76
        """Convert tasks into CSV."""
77
        if tasks is True:
78
            return
79
        for task in tasks:
80
            self.csv_converter(task.get("items", []), writer)
81
            task.pop("items", [])
82
            self.csv_converter(task.get("checklist", []), writer)
83
            task.pop("checklist", [])
84
            writer.writerow(task)
85
86
    def opml_dumps(self, tasks):
87
        """Convert tasks into OPML."""
88
89
        top = Element("opml")
90
        head = SubElement(top, "head")
91
        SubElement(head, "title").text = "Things 3 Database"
92
        body = SubElement(top, "body")
93
94
        self.opml_convert(tasks, body)
95
96
        return minidom.parseString(ETree.tostring(top)).toprettyxml(indent="   ")
97
98
    def opml_convert(self, tasks, top):
99
        """Print pretty OPML of selected tasks."""
100
101
        if tasks is True:
102
            return
103
        for task in tasks:
104
            area = SubElement(top, "outline")
105
            area.set("text", task["title"])
106
            self.opml_convert(task.get("items", []), area)
107
            task.pop("items", [])
108
            self.opml_convert(task.get("checklist", []), area)
109
            task.pop("checklist", [])
110
111
    def txt_dumps(self, tasks, indentation="", result=""):
112
        """Print pretty text version of selected tasks."""
113
114
        if tasks is True:
115
            return result
116
        for task in tasks:
117
            title = task["title"]
118
            context = (
119
                task.get("project_title", None)
120
                or task.get("area_title", None)
121
                or task.get("heading_title", None)
122
                or task.get("start", None)
123
            )
124
            start = task.get("start_date", None)
125
            details = " | ".join(filter(None, [start, context]))
126
            result = result + f"{indentation}- {title} ({details})\n"
127
            result = self.txt_dumps(task.get("items", []), indentation + "  ", result)
128
            task.pop("items", [])
129
            result = self.txt_dumps(
130
                task.get("checklist", []), indentation + "  ", result
131
            )
132
133
        return result
134
135
    @classmethod
136
    def print_unimplemented(cls, command):
137
        """Show warning that method is not yet implemented."""
138
        print("command '%s' not implemented yet" % command, file=sys.stderr)
139
140
    @classmethod
141
    def get_parser(cls):
142
        """Create command line argument parser"""
143
        parser = argparse.ArgumentParser(description="Simple read-only Thing 3 CLI.")
144
145
        subparsers = parser.add_subparsers(
146
            help="", metavar="command", required=True, dest="command"
147
        )
148
149
        ################################
150
        # Core database methods
151
        ################################
152
        subparsers.add_parser("inbox", help="Shows inbox tasks")
153
        subparsers.add_parser("today", help="Shows todays tasks")
154
        subparsers.add_parser("upcoming", help="Shows upcoming tasks")
155
        subparsers.add_parser("anytime", help="Shows anytime tasks")
156
        subparsers.add_parser("completed", help="Shows completed tasks")
157
        subparsers.add_parser("canceled", help="Shows canceled tasks")
158
        subparsers.add_parser("trash", help="Shows trashed tasks")
159
        subparsers.add_parser("todos", help="Shows all todos")
160
        subparsers.add_parser("all", help="Shows all tasks")
161
        subparsers.add_parser("areas", help="Shows all areas")
162
        subparsers.add_parser("projects", help="Shows all projects")
163
        subparsers.add_parser("logbook", help="Shows tasks completed today")
164
        subparsers.add_parser("tags", help="Shows all tags ordered by their usage")
165
        subparsers.add_parser("deadlines", help="Shows tasks with due dates")
166
167
        ################################
168
        # Additional functions
169
        ################################
170
        subparsers.add_parser("feedback", help="Give feedback")
171
        subparsers.add_parser(
172
            "search", help="Searches for a specific task"
173
        ).add_argument("string", help="String to search for")
174
175
        ################################
176
        # To be implemented in things.py
177
        ################################
178
        # subparsers.add_parser("repeating", help="Shows all repeating tasks")
179
        # subparsers.add_parser("subtasks", help="Shows all subtasks")
180
        # subparsers.add_parser("headings", help="Shows headings")
181
182
        ################################
183
        # To be converted from https://github.com/alexanderwillner/things.sh
184
        ################################
185
        # subparsers.add_parser("backlog", help="Shows backlog tasks")
186
        # subparsers.add_parser("empty", help="Shows projects that are empty")
187
        # subparsers.add_parser("hours", help="Shows hours planned today")
188
        # subparsers.add_parser("ical", help="Shows tasks ordered by due date as iCal")
189
        # subparsers.add_parser("lint", help="Shows tasks that float around")
190
        # subparsers.add_parser(
191
        #     "mostClosed", help="Shows days when most tasks were closed"
192
        # )
193
        # subparsers.add_parser(
194
        #     "mostCancelled", help="Shows days when most tasks were cancelled"
195
        # )
196
        # subparsers.add_parser(
197
        #     "mostTrashed", help="Shows days when most tasks were trashed"
198
        # )
199
        # subparsers.add_parser(
200
        #     "mostCreated", help="Shows days when most tasks were created"
201
        # )
202
        # subparsers.add_parser("mostTasks", help="Shows projects that have most tasks")
203
        # subparsers.add_parser(
204
        #     "mostCharacters", help="Shows tasks that have most characters"
205
        # )
206
        # subparsers.add_parser("nextish", help="Shows all nextish tasks")
207
        # subparsers.add_parser("old", help="Shows all old tasks")
208
        # subparsers.add_parser("schedule", help="Schedules an event using a template")
209
        # subparsers.add_parser("stat", help="Provides a number of statistics")
210
        # subparsers.add_parser("statcsv", help="Exports some statistics as CSV")
211
        # subparsers.add_parser("tag", help="Shows all tasks with the waiting for tag")
212
        # subparsers.add_parser(
213
        #     "waiting", help="Shows all tasks with the waiting for tag"
214
        # )
215
216
        ################################
217
        # To be converted from https://github.com/alexanderwillner/things.sh
218
        ################################
219
        # parser.add_argument("-a", "--anonymize",
220
        #                     action="store_true", default=False,
221
        #                     help="anonymize output", dest="anonymize")
222
223
        parser.add_argument(
224
            "-p", "--filter-project", dest="filter_project", help="Filter by project"
225
        )
226
        parser.add_argument(
227
            "-a", "--filter-area", dest="filter_area", help="Filter by area"
228
        )
229
        parser.add_argument(
230
            "-t", "--filtertag", dest="filter_tag", help="Filter by tag"
231
        )
232
233
        parser.add_argument(
234
            "-o",
235
            "--opml",
236
            action="store_true",
237
            default=False,
238
            help="output as OPML",
239
            dest="opml",
240
        )
241
242
        parser.add_argument(
243
            "-j",
244
            "--json",
245
            action="store_true",
246
            default=False,
247
            help="output as JSON",
248
            dest="json",
249
        )
250
251
        parser.add_argument(
252
            "-c",
253
            "--csv",
254
            action="store_true",
255
            default=False,
256
            help="output as CSV",
257
            dest="csv",
258
        )
259
260
        parser.add_argument(
261
            "-r",
262
            "--recursive",
263
            help="in-depth output",
264
            dest="recursive",
265
            default=False,
266
            action="store_true",
267
        )
268
269
        parser.add_argument(
270
            "-d", "--database", help="set path to database", dest="database"
271
        )
272
273
        parser.add_argument(
274
            "--version",
275
            "-v",
276
            action="version",
277
            version="%(prog)s (version {version})".format(version=__version__),
278
        )
279
280
        argcomplete.autocomplete(parser)
281
282
        return parser
283
284
    def defaults(self):
285
        """Some default options for the new API."""
286
        return dict(
287
            project=self.filter_project,
288
            area=self.filter_area,
289
            tag=self.filter_tag,
290
            include_items=self.recursive,
291
            filepath=self.database,
292
        )
293
294
    def main(self, args=None):
295
        """ Main entry point of the app """
296
297
        if args is None:
298
            self.main(ThingsCLI.get_parser().parse_args())
299
        else:
300
            command = args.command
301
            self.print_json = args.json
302
            self.print_csv = args.csv
303
            self.print_opml = args.opml
304
            self.database = args.database or self.database
305
            self.filter_project = args.filter_project or None
306
            self.filter_area = args.filter_area or None
307
            self.filter_tag = args.filter_tag or None
308
            self.recursive = args.recursive
309
            # self.anonymize = args.anonymize
310
            # self.things3.anonymize = self.anonymize ## not implemented
311
            defaults = self.defaults()
312
313
            remove_filter = ["all", "areas", "tags"]
314
            if command in remove_filter:
315
                defaults.pop("area")
316
                defaults.pop("project")
317
                defaults.pop("tag")
318
319
            if command == "all":
320
                inbox = api.inbox(**defaults)
321
                today = api.today(**defaults)
322
                upcoming = api.upcoming(**defaults)
323
                anytime = api.anytime(**defaults)
324
                someday = api.someday(**defaults)
325
                logbook = api.logbook(**defaults)
326
327
                no_area = api.projects(**defaults)
328
                areas = api.areas(**defaults)
329
                structure = [
330
                    {"title": "Inbox", "items": inbox},
331
                    {"title": "Today", "items": today},
332
                    {"title": "Upcoming", "items": upcoming},
333
                    {"title": "Anytime", "items": anytime},
334
                    {"title": "Someday", "items": someday},
335
                    {"title": "Logbook", "items": logbook},
336
                    {"title": "No Area", "items": no_area},
337
                    {"title": "Areas", "items": areas},
338
                ]
339
                self.print_tasks(structure)
340
            elif command == "upcoming":
341
                result = getattr(api, command)(**defaults)
342
                result.sort(key=lambda task: task["start_date"], reverse=False)
343
                self.print_tasks(result)
344
            elif command == "search":
345
                self.print_tasks(
346
                    api.search(
347
                        args.string,
348
                        filepath=self.database,
349
                        include_items=self.recursive,
350
                    )
351
                )
352
            elif command == "feedback":  # pragma: no cover
353
                webbrowser.open("https://github.com/thingsapi/things-cli/issues")
354
            elif command in dir(api):
355
                self.print_tasks(getattr(api, command)(**defaults))
356
            else:  # pragma: no cover
357
                ThingsCLI.print_unimplemented(command)
358
                sys.exit(3)
359
360
361
def main():
362
    """Main entry point for CLI installation"""
363
    ThingsCLI().main()
364
365
366
if __name__ == "__main__":
367
    main()
368