Passed
Push — master ( 8bfa3f...40b349 )
by Alexander
01:15
created

things_cli.cli.ThingsCLI.csv_header()   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 3
dl 0
loc 4
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
__author__ = "Alexander Willner"
9
__copyright__ = "2021 Alexander Willner"
10
__credits__ = ["Alexander Willner"]
11
__license__ = "Apache License 2.0"
12
__version__ = "0.0.4"
13
__maintainer__ = "Alexander Willner"
14
__email__ = "[email protected]"
15
__status__ = "Development"
16
17
import sys
18
import argparse
19
import json
20
import csv
21
import webbrowser
22
import argcomplete  # type: ignore
0 ignored issues
show
introduced by
Unable to import 'argcomplete'
Loading history...
23
import xml.etree.ElementTree as ET
0 ignored issues
show
introduced by
standard import "import xml.etree.ElementTree as ET" should be placed before "import argcomplete"
Loading history...
24
from xml.etree.ElementTree import Element, SubElement
0 ignored issues
show
introduced by
standard import "from xml.etree.ElementTree import Element, SubElement" should be placed before "import argcomplete"
Loading history...
25
from xml.dom import minidom
0 ignored issues
show
introduced by
standard import "from xml.dom import minidom" should be placed before "import argcomplete"
Loading history...
26
from io import StringIO
0 ignored issues
show
introduced by
standard import "from io import StringIO" should be placed before "import argcomplete"
Loading history...
27
28
import things as api
0 ignored issues
show
introduced by
Unable to import 'things'
Loading history...
29
30
31
class ThingsCLI:
32
    """A simple Python 3 CLI to read your Things app data."""
33
34
    print_json = False
35
    print_csv = False
36
    print_opml = False
37
    # anonymize = False
38
    database = None
39
    recursive = False
40
41
    def __init__(self, database=None):
42
        self.database = database
43
44
    def print_tasks(self, tasks):
45
        """Print a task."""
46
        if self.print_json:
47
            print(json.dumps(tasks))
48
        elif self.print_opml:
49
            print(self.opml_dumps(tasks))
50
        elif self.print_csv:
51
            print(self.csv_dumps(tasks))
52
        else:
53
            print(self.txt_dumps(tasks))
54
55
    def csv_dumps(self, tasks):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
56
        fieldnames = []
57
        self.csv_header(tasks, fieldnames)
58
        if 'items' in fieldnames:
59
            fieldnames.remove('items')
60
        if 'checklist' in fieldnames:
61
            fieldnames.remove('checklist')
62
63
        output = StringIO()
64
        writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=";")
65
        writer.writeheader()
66
67
        self.csv_converter(tasks, writer)
68
69
        return output.getvalue()
70
71
    def csv_header(self, tasks, fieldnames):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
72
        for task in tasks:
73
            fieldnames.extend(field for field in task if field not in fieldnames)
74
            self.csv_header(task.get('items', []), fieldnames)
75
76
    def csv_converter(self, tasks, writer):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
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):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
87
        top = Element('opml')
88
        head = SubElement(top, 'head')
89
        SubElement(head, 'title').text = 'Things 3 Database'
90
        body = SubElement(top, 'body')
91
92
        self.opml_convert(tasks, body)
93
94
        return minidom.parseString(
95
            ET.tostring(top)).toprettyxml(indent="   ")
96
97
    def opml_convert(self, tasks, top):
98
        """Print pretty OPML of selected tasks."""
99
100
        if tasks is True:
101
            return
102
        for task in tasks:
103
            area = SubElement(top, 'outline')
104
            area.set('text', task['title'])
105
            self.opml_convert(task.get('items', []), area)
106
            task.pop('items', [])
107
            self.opml_convert(task.get('checklist', []), area)
108
            task.pop('checklist', [])
109
110
    def txt_dumps(self, tasks, indentation="", result=""):
111
        """Print pretty text version of selected tasks."""
112
113
        if tasks is True:
114
            return result
115
        for task in tasks:
116
            title = task["title"]
117
            context = task.get("project_title", None) or \
118
                task.get("area_title", None) or \
119
                task.get("heading_title", None) or \
120
                task.get("start", None)
121
            start = task.get("start_date", None)
122
            details = " | ".join(filter(None, [start, context]))
123
            result = result + f"{indentation}- {title} ({details})\n"
124
            result = self.txt_dumps(task.get('items', []), indentation + "  ", result)
125
            task.pop('items', [])
126
            result = self.txt_dumps(task.get('checklist', []), indentation + "  ", result)
127
128
        return result
129
130
    @classmethod
131
    def print_unimplemented(cls, command):
132
        """Show warning that method is not yet implemented."""
133
        print("command '%s' not implemented yet" % command, file=sys.stderr)
134
135
    @classmethod
136
    def get_parser(cls):
137
        """Create command line argument parser"""
138
        parser = argparse.ArgumentParser(description="Simple read-only Thing 3 CLI.")
139
140
        subparsers = parser.add_subparsers(
141
            help="", metavar="command", required=True, dest="command"
142
        )
143
144
        ################################
145
        # Core database methods
146
        ################################
147
        subparsers.add_parser("inbox", help="Shows inbox tasks")
148
        subparsers.add_parser("today", help="Shows todays tasks")
149
        subparsers.add_parser("upcoming", help="Shows upcoming tasks")
150
        subparsers.add_parser("anytime", help="Shows anytime tasks")
151
        subparsers.add_parser("completed", help="Shows completed tasks")
152
        subparsers.add_parser("canceled", help="Shows canceled tasks")
153
        subparsers.add_parser("all", help="Shows all tasks")
154
        subparsers.add_parser("areas", help="Shows all areas")
155
        subparsers.add_parser("projects", help="Shows all projects")
156
        subparsers.add_parser("logbook", help="Shows tasks completed today")
157
        subparsers.add_parser("tags", help="Shows all tags ordered by their usage")
158
        subparsers.add_parser("deadlines", help="Shows tasks with due dates")
159
160
        ################################
161
        # Additional functions
162
        ################################
163
        subparsers.add_parser("feedback", help="Give feedback")
164
        subparsers.add_parser(
165
            "search", help="Searches for a specific task"
166
        ).add_argument("string", help="String to search for")
167
168
        ################################
169
        # To be implemented in things.py
170
        ################################
171
        # subparsers.add_parser("repeating", help="Shows all repeating tasks")
172
        # subparsers.add_parser("trashed", help="Shows trashed tasks")
173
        # subparsers.add_parser("subtasks", help="Shows all subtasks")
174
        # subparsers.add_parser("headings", help="Shows headings")
175
176
        ################################
177
        # To be converted from https://github.com/alexanderwillner/things.sh
178
        ################################
179
        # subparsers.add_parser("backlog", help="Shows backlog tasks")
180
        # subparsers.add_parser("empty", help="Shows projects that are empty")
181
        # subparsers.add_parser("hours", help="Shows hours planned today")
182
        # subparsers.add_parser("ical", help="Shows tasks ordered by due date as iCal")
183
        # subparsers.add_parser("lint", help="Shows tasks that float around")
184
        # subparsers.add_parser(
185
        #     "mostClosed", help="Shows days when most tasks were closed"
186
        # )
187
        # subparsers.add_parser(
188
        #     "mostCancelled", help="Shows days when most tasks were cancelled"
189
        # )
190
        # subparsers.add_parser(
191
        #     "mostTrashed", help="Shows days when most tasks were trashed"
192
        # )
193
        # subparsers.add_parser(
194
        #     "mostCreated", help="Shows days when most tasks were created"
195
        # )
196
        # subparsers.add_parser("mostTasks", help="Shows projects that have most tasks")
197
        # subparsers.add_parser(
198
        #     "mostCharacters", help="Shows tasks that have most characters"
199
        # )
200
        # subparsers.add_parser("nextish", help="Shows all nextish tasks")
201
        # subparsers.add_parser("old", help="Shows all old tasks")
202
        # subparsers.add_parser("schedule", help="Schedules an event using a template")
203
        # subparsers.add_parser("stat", help="Provides a number of statistics")
204
        # subparsers.add_parser("statcsv", help="Exports some statistics as CSV")
205
        # subparsers.add_parser("tag", help="Shows all tasks with the waiting for tag")
206
        # subparsers.add_parser(
207
        #     "waiting", help="Shows all tasks with the waiting for tag"
208
        # )
209
210
        ################################
211
        # To be converted from https://github.com/alexanderwillner/things.sh
212
        ################################
213
        # parser.add_argument("-a", "--anonymize",
214
        #                     action="store_true", default=False,
215
        #                     help="anonymize output", dest="anonymize")
216
217
        parser.add_argument(
218
            "-o",
219
            "--opml",
220
            action="store_true",
221
            default=False,
222
            help="output as OPML",
223
            dest="opml")
224
225
        parser.add_argument(
226
            "-j",
227
            "--json",
228
            action="store_true",
229
            default=False,
230
            help="output as JSON",
231
            dest="json",
232
        )
233
234
        parser.add_argument(
235
            "-c",
236
            "--csv",
237
            action="store_true",
238
            default=False,
239
            help="output as CSV",
240
            dest="csv",
241
        )
242
243
        parser.add_argument(
244
            "-r", "--recursive", help="in-depth output", dest="recursive", default=False, action="store_true"
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (109/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
245
        )
246
247
        parser.add_argument(
248
            "-d", "--database", help="set path to database", dest="database"
249
        )
250
251
        parser.add_argument(
252
            "--version",
253
            "-v",
254
            action="version",
255
            version="%(prog)s (version {version})".format(version=__version__),
256
        )
257
258
        argcomplete.autocomplete(parser)
259
260
        return parser
261
262
    def main(self, args=None):
263
        """ Main entry point of the app """
264
265
        if args is None:
266
            self.main(ThingsCLI.get_parser().parse_args())
267
        else:
268
            command = args.command
269
            self.print_json = args.json
270
            self.print_csv = args.csv
271
            self.print_opml = args.opml
272
            self.database = (
273
                args.database if args.database is not None else self.database
274
            )
275
            self.recursive = args.recursive
276
            # self.anonymize = args.anonymize
277
            # self.things3.anonymize = self.anonymize ## not implemented
278
279
            if command == "all":
280
                inbox = api.inbox(filepath=self.database, include_items=self.recursive)
281
                today = api.today(filepath=self.database, include_items=self.recursive)
282
                upcoming = api.upcoming(filepath=self.database, include_items=self.recursive)
283
                anytime = api.anytime(filepath=self.database, include_items=self.recursive)
284
                someday = api.someday(filepath=self.database, include_items=self.recursive)
285
                logbook = api.logbook(filepath=self.database, include_items=self.recursive)
286
                no_area = api.projects(area=False, filepath=self.database, include_items=self.recursive)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
287
                areas = api.areas(filepath=self.database, include_items=self.recursive)
288
                structure = [{"title": "Inbox",
289
                              "items": inbox},
290
                             {"title": "Today",
291
                              "items": today},
292
                             {"title": "Upcoming",
293
                              "items": upcoming},
294
                             {"title": "Anytime",
295
                              "items": anytime},
296
                             {"title": "Someday",
297
                              "items": someday},
298
                             {"title": "Logbook",
299
                              "items": logbook},
300
                             {"title": "No Area",
301
                              "items": no_area},
302
                             {"title": "Areas",
303
                              "items": areas}
304
                             ]
305
                self.print_tasks(structure)
306
            elif command == "upcoming":
307
                result = getattr(api, command)(filepath=self.database,
308
                                               include_items=self.recursive)
309
                result.sort(key=lambda task: task["start_date"], reverse=False)
310
                self.print_tasks(result)
311
            elif command == "search":
312
                self.print_tasks(api.search(args.string, filepath=self.database,
313
                                            include_items=self.recursive))
314
            elif command == "feedback":  # pragma: no cover
315
                webbrowser.open("https://github.com/thingsapi/things-cli/issues")
316
            elif command in dir(api):
317
                self.print_tasks(
318
                    getattr(api, command)(filepath=self.database,
319
                                          include_items=self.recursive))
320
            else:  # pragma: no cover
321
                ThingsCLI.print_unimplemented(command)
322
                sys.exit(3)
323
324
325
def main():
326
    """Main entry point for CLI installation"""
327
    ThingsCLI().main()
328
329
330
if __name__ == "__main__":
331
    main()
332