Passed
Push — master ( 061a09...1465d6 )
by Alexander
01:50
created

things3.things3.Things3.defaults()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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