Passed
Push — master ( c7c9ef...a777de )
by Alexander
05:19
created

things3.things3.Things3.reset_config()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""Simple read-only API for Things 3."""
5
6
from __future__ import print_function
7
8
9
import sqlite3
10
import sys
11
from random import shuffle
12
import os
13
from os import environ, path
14
import getpass
15
import configparser
16
from pathlib import Path
17
18
# the new core library, migration ongoing
19
import things
20
21
# pylint: disable=R0904,R0902
22
23
24
class Things3:
25
    """Simple read-only API for Things 3."""
26
27
    # Database info
28
    FILE_CONFIG = str(Path.home()) + "/.kanbanviewrc"
29
    FILE_DB = (
30
        "/Library/Group Containers/"
31
        "JLMPQHK86H.com.culturedcode.ThingsMac/"
32
        "Things Database.thingsdatabase/main.sqlite"
33
    )
34
    TABLE_TASK = "TMTask"
35
    TABLE_AREA = "TMArea"
36
    TABLE_TAG = "TMTag"
37
    TABLE_TASKTAG = "TMTaskTag"
38
    DATE_CREATE = "creationDate"
39
    DATE_MOD = "userModificationDate"
40
    DATE_DUE = "dueDate"
41
    DATE_START = "startDate"
42
    DATE_STOP = "stopDate"
43
    IS_INBOX = "start = 0"  # noqa
44
    IS_ANYTIME = "start = 1"
45
    IS_SOMEDAY = "start = 2"
46
    IS_SCHEDULED = f"{DATE_START} IS NOT NULL"
47
    IS_NOT_SCHEDULED = f"{DATE_START} IS NULL"
48
    IS_DUE = f"{DATE_DUE} IS NOT NULL"  # noqa
49
    IS_RECURRING = "recurrenceRule IS NOT NULL"
50
    IS_NOT_RECURRING = "recurrenceRule IS NULL"  # noqa
51
    IS_TASK = "type = 0"
52
    IS_PROJECT = "type = 1"
53
    IS_HEADING = "type = 2"
54
    IS_TRASHED = "trashed = 1"
55
    IS_NOT_TRASHED = "trashed = 0"
56
    IS_OPEN = "status = 0"
57
    IS_CANCELLED = "status = 2"
58
    IS_DONE = "status = 3"
59
    RECURRING_IS_NOT_PAUSED = "instanceCreationPaused = 0"
60
    RECURRING_HAS_NEXT_STARTDATE = "nextInstanceStartDate IS NOT NULL"
61
    MODE_TASK = "type = 0"
62
    MODE_PROJECT = "type = 1"
63
64
    # Variables
65
    debug = False
66
    user = getpass.getuser()
67
    database = f"/Users/{user}/{FILE_DB}"
68
    filter = ""
69
    tag_waiting = "Waiting"
70
    tag_mit = "MIT"
71
    tag_cleanup = "Cleanup"
72
    tag_a = "A"
73
    tag_b = "B"
74
    tag_c = "C"
75
    tag_d = "D"
76
    stat_days = 365
77
    anonymize = False
78
    config = configparser.ConfigParser()
79
    config.read(FILE_CONFIG)
80
    mode = "to-do"
81
    filter_project = None
82
    filter_area = None
83
84
    # pylint: disable=R0913
85
    def __init__(
86
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
87
        database=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
88
        tag_waiting=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
89
        tag_mit=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
90
        tag_cleanup=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
91
        tag_a=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
92
        tag_b=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
93
        tag_c=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
94
        tag_d=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
95
        stat_days=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
96
        anonymize=None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
97
    ):
98
99
        cfg = self.get_from_config(tag_waiting, "TAG_WAITING")
100
        self.tag_waiting = cfg if cfg else self.tag_waiting
101
        self.set_config("TAG_WAITING", self.tag_waiting)
102
103
        cfg = self.get_from_config(anonymize, "ANONYMIZE")
104
        self.anonymize = (cfg == "True") if (cfg == "True") else self.anonymize
105
        self.set_config("ANONYMIZE", self.anonymize)
106
107
        cfg = self.get_from_config(tag_mit, "TAG_MIT")
108
        self.tag_mit = cfg if cfg else self.tag_mit
109
        self.set_config("TAG_MIT", self.tag_mit)
110
111
        cfg = self.get_from_config(tag_cleanup, "TAG_CLEANUP")
112
        self.tag_cleanup = cfg if cfg else self.tag_cleanup
113
        self.set_config("TAG_CLEANUP", self.tag_cleanup)
114
115
        cfg = self.get_from_config(tag_a, "TAG_A")
116
        self.tag_a = cfg if cfg else self.tag_a
117
        self.set_config("TAG_A", self.tag_a)
118
119
        cfg = self.get_from_config(tag_b, "TAG_B")
120
        self.tag_b = cfg if cfg else self.tag_b
121
        self.set_config("TAG_B", self.tag_b)
122
123
        cfg = self.get_from_config(tag_c, "TAG_C")
124
        self.tag_c = cfg if cfg else self.tag_c
125
        self.set_config("TAG_C", self.tag_c)
126
127
        cfg = self.get_from_config(tag_d, "TAG_D")
128
        self.tag_d = cfg if cfg else self.tag_d
129
        self.set_config("TAG_D", self.tag_d)
130
131
        cfg = self.get_from_config(stat_days, "STAT_DAYS")
132
        self.stat_days = cfg if cfg else self.stat_days
133
        self.set_config("STAT_DAYS", self.stat_days)
134
135
        cfg = self.get_from_config(database, "THINGSDB")
136
        self.database = cfg if cfg else self.database
137
        # Automated migration to new database location in Things 3.12.6/3.13.1
138
        # --------------------------------
139
        try:
140
            with open(self.database, encoding="utf-8") as f_d:
141
                if "Your database file has been moved there" in f_d.readline():
142
                    self.database = f"/Users/{self.user}/{self.FILE_DB}"
143
        except (UnicodeDecodeError, FileNotFoundError, PermissionError):
144
            pass  # binary file (old database) or doesn't exist
145
        # --------------------------------
146
        self.set_config("THINGSDB", self.database)
147
148
    def set_config(self, key, value, domain="DATABASE"):
149
        """Write variable to config."""
150
        if domain not in self.config:
151
            self.config.add_section(domain)
152
        if value is not None and key is not None:
153
            self.config.set(domain, str(key), str(value))
154
            with open(self.FILE_CONFIG, "w+", encoding="utf-8") as configfile:
155
                self.config.write(configfile)
156
157
    def get_config(self, key, domain="DATABASE"):
158
        """Get variable from config."""
159
        result = None
160
        if domain in self.config and key in self.config[domain]:
161
            result = path.expanduser(self.config[domain][key])
162
        return result
163
164
    def get_from_config(self, variable, key, domain="DATABASE"):
165
        """Set variable. Priority: input, environment, config"""
166
        result = None
167
        if variable is not None:
168
            result = variable
169
        elif environ.get(key):
170
            result = environ.get(key)
171
        elif domain in self.config and key in self.config[domain]:
172
            result = path.expanduser(self.config[domain][key])
173
        return result
174
175
    @staticmethod
176
    def anonymize_string(string):
177
        """Scramble text."""
178
        if string is None:
179
            return None
180
        string = list(string)
181
        shuffle(string)
182
        string = "".join(string)
183
        return string
184
185
    @staticmethod
186
    def dict_factory(cursor, row):
187
        """Convert SQL result into a dictionary"""
188
        dictionary = {}
189
        for idx, col in enumerate(cursor.description):
190
            dictionary[col[0]] = row[idx]
191
        return dictionary
192
193
    def anonymize_tasks(self, tasks):
194
        """Scramble output for screenshots."""
195
        if self.anonymize:
196
            for task in tasks:
197
                task["title"] = self.anonymize_string(task["title"])
198
                task["context"] = (
199
                    self.anonymize_string(task["context"]) if "context" in task else ""
200
                )
201
        return tasks
202
203
    def defaults(self):
204
        """Some default options for the new API."""
205
        return dict(
206
            type=self.mode,
207
            project=self.filter_project,
208
            area=self.filter_area,
209
            filepath=self.database,
210
        )
211
212
    def convert_new_things_lib(self, tasks):
213
        """Convert tasks from new library to old expectations."""
214
        for task in tasks:
215
            task["context"] = (
216
                task.get("project_title")
217
                or task.get("area_title")
218
                or task.get("heading_title")
219
            )
220
            task["context_uuid"] = (
221
                task.get("project") or task.get("area") or task.get("heading")
222
            )
223
            task["due"] = task.get("deadline")
224
            task["started"] = task.get("start_date")
225
            task["size"] = things.projects(
226
                task["uuid"], count_only=True, filepath=self.database
227
            )
228
        tasks.sort(key=lambda task: task["title"] or "", reverse=False)
229
        tasks = self.anonymize_tasks(tasks)
230
        return tasks
231
232
    def get_inbox(self):
233
        """Get tasks from inbox."""
234
        tasks = things.inbox(**self.defaults())
235
        tasks = self.convert_new_things_lib(tasks)
236
        return tasks
237
238
    def get_today(self):
239
        """Get tasks from today."""
240
        tasks = things.today(**self.defaults())
241
        tasks = self.convert_new_things_lib(tasks)
242
        tasks.sort(key=lambda task: task.get("started", ""), reverse=True)
243
        tasks.sort(key=lambda task: task.get("todayIndex", ""), reverse=False)
244
        return tasks
245
246
    def get_task(self, area=None, project=None):
247
        """Get tasks."""
248
        tasks = things.tasks(area=area, project=project, filepath=self.database)
249
        tasks = self.convert_new_things_lib(tasks)
250
        return tasks
251
252
    def get_someday(self):
253
        """Get someday tasks."""
254
        tasks = things.someday(**self.defaults())
255
        tasks = self.convert_new_things_lib(tasks)
256
        tasks.sort(key=lambda task: task["deadline"] or "", reverse=True)
257
        return tasks
258
259
    def get_upcoming(self):
260
        """Get upcoming tasks."""
261
        tasks = things.upcoming(**self.defaults())
262
        tasks = self.convert_new_things_lib(tasks)
263
        tasks.sort(key=lambda task: task["started"] or "", reverse=False)
264
        return tasks
265
266
    def get_waiting(self):
267
        """Get waiting tasks."""
268
        tasks = self.get_tag(self.tag_waiting)
269
        tasks.sort(key=lambda task: task["started"] or "", reverse=False)
270
        return tasks
271
272
    def get_mit(self):
273
        """Get most important tasks."""
274
        return self.get_tag(self.tag_mit)
275
276
    def get_tag(self, tag):
277
        """Get task with specific tag."""
278
        try:
279
            tasks = things.tasks(tag=tag, **self.defaults())
280
            tasks = self.convert_new_things_lib(tasks)
281
        except ValueError:
282
            tasks = []
283
        if tag in [self.tag_waiting]:
284
            tasks.sort(key=lambda task: task["started"] or "", reverse=False)
285
        return tasks
286
287
    def get_tag_today(self, tag):
288
        """Get today tasks with specific tag."""
289
        tasks = things.today(tag=tag, **self.defaults())
290
        tasks = self.convert_new_things_lib(tasks)
291
        return tasks
292
293
    def get_anytime(self):
294
        """Get anytime tasks."""
295
        query = f"""
296
                TASK.{self.IS_NOT_TRASHED} AND
297
                TASK.{self.IS_TASK} AND
298
                TASK.{self.IS_OPEN} AND
299
                TASK.{self.IS_ANYTIME} AND
300
                TASK.{self.IS_NOT_SCHEDULED} AND (
301
                    (
302
                        PROJECT.title IS NULL OR (
303
                            PROJECT.{self.IS_ANYTIME} AND
304
                            PROJECT.{self.IS_NOT_SCHEDULED} AND
305
                            PROJECT.{self.IS_NOT_TRASHED}
306
                        )
307
                    ) AND (
308
                        HEADPROJ.title IS NULL OR (
309
                            HEADPROJ.{self.IS_ANYTIME} AND
310
                            HEADPROJ.{self.IS_NOT_SCHEDULED} AND
311
                            HEADPROJ.{self.IS_NOT_TRASHED}
312
                        )
313
                    )
314
                )
315
                ORDER BY TASK.duedate DESC , TASK.todayIndex
316
                """
317
        if self.filter:
318
            # ugly hack for Kanban task view on project
319
            query = f"""
320
                TASK.{self.IS_NOT_TRASHED} AND
321
                TASK.{self.IS_TASK} AND
322
                TASK.{self.IS_OPEN} AND
323
                TASK.{self.IS_ANYTIME} AND
324
                TASK.{self.IS_NOT_SCHEDULED} AND (
325
                    (
326
                        PROJECT.title IS NULL OR (
327
                            PROJECT.{self.IS_NOT_TRASHED}
328
                        )
329
                    ) AND (
330
                        HEADPROJ.title IS NULL OR (
331
                            HEADPROJ.{self.IS_NOT_TRASHED}
332
                        )
333
                    )
334
                )
335
                ORDER BY TASK.duedate DESC , TASK.todayIndex
336
                """
337
        return self.get_rows(query)
338
339
    def get_completed(self):
340
        """Get completed tasks."""
341
        tasks = things.completed(**self.defaults())
342
        tasks = self.convert_new_things_lib(tasks)
343
        return tasks
344
345
    def get_cancelled(self):
346
        """Get cancelled tasks."""
347
        tasks = things.canceled(**self.defaults())
348
        tasks = self.convert_new_things_lib(tasks)
349
        return tasks
350
351
    def get_trashed(self):
352
        """Get trashed tasks."""
353
        query = f"""
354
                TASK.{self.IS_TRASHED} AND
355
                TASK.{self.IS_TASK}
356
                ORDER BY TASK.{self.DATE_STOP}
357
                """
358
        return self.get_rows(query)
359
360
    def get_projects(self, area=None):
361
        """Get projects."""
362
        tasks = things.projects(area=area, filepath=self.database)
363
        tasks = self.convert_new_things_lib(tasks)
364
        return tasks
365
366
    def get_areas(self):
367
        """Get areas."""
368
        tasks = things.areas(filepath=self.database)
369
        tasks = self.convert_new_things_lib(tasks)
370
        for task in tasks:
371
            task["size"] = things.areas(
372
                task["uuid"], count_only=True, filepath=self.database
373
            )
374
        return tasks
375
376
    def get_all(self):
377
        """Get all tasks."""
378
        tasks = things.tasks(**self.defaults())
379
        tasks = self.convert_new_things_lib(tasks)
380
        return tasks
381
382
    def get_due(self):
383
        """Get due tasks."""
384
        tasks = things.deadlines(**self.defaults())
385
        tasks = self.convert_new_things_lib(tasks)
386
        tasks.sort(key=lambda task: task["deadline"] or "", reverse=False)
387
        return tasks
388
389
    def get_lint(self):
390
        """Get tasks that float around"""
391
        query = f"""
392
            TASK.{self.IS_NOT_TRASHED} AND
393
            TASK.{self.IS_OPEN} AND
394
            TASK.{self.IS_TASK} AND
395
            (TASK.{self.IS_SOMEDAY} OR TASK.{self.IS_ANYTIME}) AND
396
            TASK.project IS NULL AND
397
            TASK.area IS NULL AND
398
            TASK.actionGroup IS NULL
399
            """
400
        return self.get_rows(query)
401
402
    def get_empty_projects(self):
403
        """Get projects that are empty"""
404
        query = f"""
405
            TASK.{self.IS_NOT_TRASHED} AND
406
            TASK.{self.IS_OPEN} AND
407
            TASK.{self.IS_PROJECT} AND
408
            TASK.{self.IS_ANYTIME}
409
            GROUP BY TASK.uuid
410
            HAVING
411
                (SELECT COUNT(uuid)
412
                 FROM TMTask AS PROJECT_TASK
413
                 WHERE
414
                   PROJECT_TASK.project = TASK.uuid AND
415
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
416
                   PROJECT_TASK.{self.IS_OPEN} AND
417
                   (PROJECT_TASK.{self.IS_ANYTIME} OR
418
                    PROJECT_TASK.{self.IS_SCHEDULED} OR
419
                      (PROJECT_TASK.{self.IS_RECURRING} AND
420
                       PROJECT_TASK.{self.RECURRING_IS_NOT_PAUSED} AND
421
                       PROJECT_TASK.{self.RECURRING_HAS_NEXT_STARTDATE}
422
                      )
423
                   )
424
                ) = 0
425
            """
426
        return self.get_rows(query)
427
428
    def get_largest_projects(self):
429
        """Get projects that are empty"""
430
        query = f"""
431
            SELECT
432
                TASK.uuid,
433
                TASK.title AS title,
434
                {self.DATE_CREATE} AS created,
435
                {self.DATE_MOD} AS modified,
436
                (SELECT COUNT(uuid)
437
                 FROM TMTask AS PROJECT_TASK
438
                 WHERE
439
                   PROJECT_TASK.project = TASK.uuid AND
440
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
441
                   PROJECT_TASK.{self.IS_OPEN}
442
                ) AS tasks
443
            FROM
444
                {self.TABLE_TASK} AS TASK
445
            WHERE
446
               TASK.{self.IS_NOT_TRASHED} AND
447
               TASK.{self.IS_OPEN} AND
448
               TASK.{self.IS_PROJECT}
449
            GROUP BY TASK.uuid
450
            ORDER BY tasks COLLATE NOCASE DESC
451
            """
452
        return self.execute_query(query)
453
454
    def get_daystats(self):
455
        """Get a history of task activities"""
456
        query = f"""
457
                WITH RECURSIVE timeseries(x) AS (
458
                    SELECT 0
459
                    UNION ALL
460
                    SELECT x+1 FROM timeseries
461
                    LIMIT {self.stat_days}
462
                )
463
                SELECT
464
                    date(julianday("now", "-{self.stat_days} days"),
465
                         "+" || x || " days") as date,
466
                    CREATED.TasksCreated as created,
467
                    CLOSED.TasksClosed as completed,
468
                    CANCELLED.TasksCancelled as cancelled,
469
                    TRASHED.TasksTrashed as trashed
470
                FROM timeseries
471
                LEFT JOIN
472
                    (SELECT COUNT(uuid) AS TasksCreated,
473
                        date({self.DATE_CREATE},"unixepoch") AS DAY
474
                        FROM {self.TABLE_TASK} AS TASK
475
                        WHERE DAY NOT NULL
476
                          AND TASK.{self.IS_TASK}
477
                        GROUP BY DAY)
478
                    AS CREATED ON CREATED.DAY = date
479
                LEFT JOIN
480
                    (SELECT COUNT(uuid) AS TasksCancelled,
481
                        date(stopDate,"unixepoch") AS DAY
482
                        FROM {self.TABLE_TASK} AS TASK
483
                        WHERE DAY NOT NULL
484
                          AND TASK.{self.IS_CANCELLED} AND TASK.{self.IS_TASK}
485
                        GROUP BY DAY)
486
                        AS CANCELLED ON CANCELLED.DAY = date
487
                LEFT JOIN
488
                    (SELECT COUNT(uuid) AS TasksTrashed,
489
                        date({self.DATE_MOD},"unixepoch") AS DAY
490
                        FROM {self.TABLE_TASK} AS TASK
491
                        WHERE DAY NOT NULL
492
                          AND TASK.{self.IS_TRASHED} AND TASK.{self.IS_TASK}
493
                        GROUP BY DAY)
494
                        AS TRASHED ON TRASHED.DAY = date
495
                LEFT JOIN
496
                    (SELECT COUNT(uuid) AS TasksClosed,
497
                        date(stopDate,"unixepoch") AS DAY
498
                        FROM {self.TABLE_TASK} AS TASK
499
                        WHERE DAY NOT NULL
500
                          AND TASK.{self.IS_DONE} AND TASK.{self.IS_TASK}
501
                        GROUP BY DAY)
502
                        AS CLOSED ON CLOSED.DAY = date
503
                """
504
        return self.execute_query(query)
505
506
    def get_minutes_today(self):
507
        """Count the planned minutes for today."""
508
509
        tasks = things.today(**self.defaults())
510
        tasks = self.convert_new_things_lib(tasks)
511
        minutes = 0
512
        for task in tasks:
513
            for tag in task.get("tags", []):
514
                try:
515
                    minutes += int(tag)
516
                except ValueError:
517
                    pass
518
        return [{"minutes": minutes}]
519
520
    def get_cleanup(self):
521
        """Tasks and projects that need work."""
522
        result = []
523
        result.extend(self.get_lint())
524
        result.extend(self.get_empty_projects())
525
        result.extend(self.get_tag(self.tag_cleanup))
526
        result = [i for n, i in enumerate(result) if i not in result[n + 1 :]]
527
        return result
528
529
    def reset_config(self):
530
        """Reset the configuration."""
531
        print("Deleting: " + self.FILE_CONFIG)
532
        os.remove(self.FILE_CONFIG)
533
534
    @staticmethod
535
    def get_not_implemented():
536
        """Not implemented warning."""
537
        return [{"title": "not implemented"}]
538
539
    def get_rows(self, sql):
540
        """Query Things database."""
541
542
        sql = f"""
543
            SELECT DISTINCT
544
                TASK.uuid,
545
                TASK.title,
546
                CASE
547
                    WHEN AREA.title IS NOT NULL THEN AREA.title
548
                    WHEN PROJECT.title IS NOT NULL THEN PROJECT.title
549
                    WHEN HEADING.title IS NOT NULL THEN HEADING.title
550
                END AS context,
551
                CASE
552
                    WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
553
                    WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.uuid
554
                END AS context_uuid,
555
                CASE
556
                    WHEN TASK.recurrenceRule IS NULL
557
                    THEN strftime('%d.%m.', TASK.dueDate,"unixepoch") ||
558
                         substr(strftime('%Y', TASK.dueDate,"unixepoch"),3, 2)
559
                ELSE NULL
560
                END AS due,
561
                date(TASK.{self.DATE_CREATE},"unixepoch") as created,
562
                date(TASK.{self.DATE_MOD},"unixepoch") as modified,
563
                strftime('%d.%m.', TASK.startDate,"unixepoch") ||
564
                  substr(strftime('%Y', TASK.startDate,"unixepoch"),3, 2)
565
                  as started,
566
                date(TASK.stopDate,"unixepoch") as stopped,
567
                (SELECT COUNT(uuid)
568
                 FROM TMTask AS PROJECT_TASK
569
                 WHERE
570
                   PROJECT_TASK.project = TASK.uuid AND
571
                   PROJECT_TASK.{self.IS_NOT_TRASHED} AND
572
                   PROJECT_TASK.{self.IS_OPEN}
573
                ) AS size,
574
                CASE
575
                    WHEN TASK.{self.IS_TASK} THEN 'task'
576
                    WHEN TASK.{self.IS_PROJECT} THEN 'project'
577
                    WHEN TASK.{self.IS_HEADING} THEN 'heading'
578
                END AS type,
579
                CASE
580
                    WHEN TASK.{self.IS_OPEN} THEN 'open'
581
                    WHEN TASK.{self.IS_CANCELLED} THEN 'cancelled'
582
                    WHEN TASK.{self.IS_DONE} THEN 'done'
583
                END AS status,
584
                TASK.notes
585
            FROM
586
                {self.TABLE_TASK} AS TASK
587
            LEFT OUTER JOIN
588
                {self.TABLE_TASK} PROJECT ON TASK.project = PROJECT.uuid
589
            LEFT OUTER JOIN
590
                {self.TABLE_AREA} AREA ON TASK.area = AREA.uuid
591
            LEFT OUTER JOIN
592
                {self.TABLE_TASK} HEADING ON TASK.actionGroup = HEADING.uuid
593
            LEFT OUTER JOIN
594
                {self.TABLE_TASK} HEADPROJ ON HEADING.project = HEADPROJ.uuid
595
            LEFT OUTER JOIN
596
                {self.TABLE_TASKTAG} TAGS ON TASK.uuid = TAGS.tasks
597
            LEFT OUTER JOIN
598
                {self.TABLE_TAG} TAG ON TAGS.tags = TAG.uuid
599
            WHERE
600
                {self.filter}
601
                {sql}
602
                """
603
604
        return self.execute_query(sql)
605
606
    def execute_query(self, sql):
607
        """Run the actual query"""
608
        if self.debug is True:
609
            print(self.database)
610
            print(sql)
611
        try:
612
            connection = sqlite3.connect(  # pylint: disable=E1101
613
                "file:" + self.database + "?mode=ro", uri=True
614
            )
615
            connection.row_factory = Things3.dict_factory
616
            cursor = connection.cursor()
617
            cursor.execute(sql)
618
            tasks = cursor.fetchall()
619
            tasks = self.anonymize_tasks(tasks)
620
            if self.debug:
621
                for task in tasks:
622
                    print(task)
623
            return tasks
624
        except sqlite3.OperationalError as error:  # pylint: disable=E1101
625
            print(f"Could not query the database at: {self.database}.")
626
            print(f"Details: {error}.")
627
            sys.exit(2)
628
629
    # pylint: disable=C0103
630
    def mode_project(self):
631
        """Hack to switch to project view"""
632
        self.mode = "project"
633
        self.IS_TASK = self.MODE_PROJECT
634
635
    # pylint: disable=C0103
636
    def mode_task(self):
637
        """Hack to switch to project view"""
638
        self.mode = "to-do"
639
        self.IS_TASK = self.MODE_TASK
640
641
    functions = {
642
        "inbox": get_inbox,
643
        "today": get_today,
644
        "next": get_anytime,
645
        "backlog": get_someday,
646
        "upcoming": get_upcoming,
647
        "waiting": get_waiting,
648
        "mit": get_mit,
649
        "completed": get_completed,
650
        "cancelled": get_cancelled,
651
        "trashed": get_trashed,
652
        "projects": get_projects,
653
        "areas": get_areas,
654
        "all": get_all,
655
        "due": get_due,
656
        "lint": get_lint,
657
        "empty": get_empty_projects,
658
        "cleanup": get_cleanup,
659
        "top-proj": get_largest_projects,
660
        "stats-day": get_daystats,
661
        "stats-min-today": get_minutes_today,
662
        "reset": reset_config,
663
    }
664