things-py 0.0.4__py3.9.egg → 0.0.6__py3.9.egg

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
EGG-INFO/PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: things.py
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: A simple Python 3 library to read your Things app data.
5
5
  Home-page: https://github.com/thingsapi/things.py
6
6
  Author: Alexander Willner, Michael B.
Binary file
tests/test_things.py CHANGED
@@ -29,7 +29,7 @@ class ThingsCase(unittest.TestCase):
29
29
  def test_upcoming(self):
30
30
  """Test upcoming."""
31
31
  tasks = things.upcoming(**FILEPATH)
32
- self.assertEqual(2, len(tasks))
32
+ self.assertEqual(1, len(tasks))
33
33
 
34
34
  def test_deadlines(self):
35
35
  """Test deadlines."""
@@ -39,7 +39,7 @@ class ThingsCase(unittest.TestCase):
39
39
  def test_today(self):
40
40
  """Test today."""
41
41
  tasks = things.today(**FILEPATH)
42
- self.assertEqual(1, len(tasks))
42
+ self.assertEqual(2, len(tasks))
43
43
 
44
44
  def test_checklist(self):
45
45
  """Test checklist."""
@@ -47,13 +47,12 @@ class ThingsCase(unittest.TestCase):
47
47
  self.assertEqual(3, len(tasks))
48
48
  tasks = things.checklist_items('K9bx7h1xCJdevvyWardZDq', **FILEPATH)
49
49
  self.assertEqual(0, len(tasks))
50
-
51
50
 
52
51
  def test_anytime(self):
53
- """Test antime."""
52
+ """Test anytime."""
54
53
  tasks = things.anytime(**FILEPATH)
55
- self.assertEqual(8, len(tasks))
56
- self.assertTrue(any(task['area_title'] == 'Area 1' for task in tasks))
54
+ self.assertEqual(11, len(tasks))
55
+ self.assertTrue(any(task.pop('area_title', '') == 'Area 1' for task in tasks))
57
56
 
58
57
  def test_logbook(self):
59
58
  """Test logbook."""
@@ -87,22 +86,25 @@ class ThingsCase(unittest.TestCase):
87
86
  def test_todos(self):
88
87
  """Test all tasks."""
89
88
  tasks = things.todos(start="Anytime", **FILEPATH)
90
- self.assertEqual(5, len(tasks))
89
+ self.assertEqual(8, len(tasks))
91
90
  tasks = things.todos(start="Anytime", status="completed", **FILEPATH)
92
91
  self.assertEqual(6, len(tasks))
93
92
  tasks = things.todos(status="completed", **FILEPATH)
94
93
  self.assertEqual(10, len(tasks))
95
94
  tasks = things.todos(include_items=True, **FILEPATH)
96
- self.assertEqual(10, len(tasks))
95
+ self.assertEqual(12, len(tasks))
97
96
  with self.assertRaises(ValueError):
98
97
  things.todos(status="wrong_value", **FILEPATH)
99
98
  tasks = things.tasks("A2oPvtt4dXoypeoLc8uYzY", **FILEPATH)
100
- self.assertEqual(14, len(tasks))
99
+ self.assertEqual(16, len(tasks))
101
100
 
102
101
  def test_tags(self):
103
102
  """Test all tags."""
104
103
  tags = things.tags(**FILEPATH)
105
104
  self.assertEqual(5, len(tags))
105
+ tasks = things.tasks(tag='Errand', **FILEPATH)
106
+ self.assertEqual(1, len(tasks))
107
+
106
108
 
107
109
  def test_projects(self):
108
110
  """Test all projects."""
Binary file
Binary file
Binary file
things/api.py CHANGED
@@ -4,6 +4,8 @@
4
4
  Module implementing Things API.
5
5
  """
6
6
 
7
+ __version__ = "0.0.6"
8
+
7
9
  import os
8
10
  from shlex import quote
9
11
  import sys
@@ -111,8 +113,8 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
111
113
 
112
114
  filepath : str, optional
113
115
  Any valid path of a SQLite database file generated by the Things app.
114
- If no path is provided, then access the default database path.
115
- If environment variable THINGSDB is set, this path will be used.
116
+ If the environment variable THINGSDB is set, then that path will be used.
117
+ Otherwise, access the default database path.
116
118
 
117
119
  database : things.database.Database, optional
118
120
  Any valid `things.database.Database` object previously instantiated.
@@ -174,13 +176,6 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
174
176
  )
175
177
  # to-dos without headings appear before headings in app
176
178
  items.sort(key=lambda item: item["type"], reverse=True)
177
- elif task["type"] == "area":
178
- area = task
179
- area["items"] = items = tasks(
180
- area=area["uuid"],
181
- include_items=True,
182
- database=database,
183
- )
184
179
  elif task["type"] == "heading":
185
180
  heading = task
186
181
  heading["items"] = tasks(
@@ -421,7 +416,8 @@ def checklist_items(uuid, **kwargs):
421
416
  list of dict
422
417
  Representing checklist items. Or [] if none found.
423
418
  """
424
- return tasks(uuid=uuid, type="to-do", **kwargs).get('checklist', [])
419
+ return tasks(uuid=uuid, type="to-do", **kwargs).get("checklist", [])
420
+
425
421
 
426
422
  # Filter by collections in the Things app sidebar.
427
423
 
@@ -497,6 +493,17 @@ def logbook(**kwargs):
497
493
  return result
498
494
 
499
495
 
496
+ def trash(**kwargs):
497
+ """
498
+ Read trashed tasks into dicts.
499
+
500
+ See `things.api.tasks` for details.
501
+ """
502
+ result = [*canceled(**kwargs), *completed(**kwargs)]
503
+ result.sort(key=lambda task: task["stop_date"], reverse=True)
504
+ return result
505
+
506
+
500
507
  # Filter by various task properties
501
508
 
502
509
 
things/database.py CHANGED
@@ -13,16 +13,16 @@ __author__ = "Alexander Willner"
13
13
  __copyright__ = "2020 Alexander Willner"
14
14
  __credits__ = ["Alexander Willner"]
15
15
  __license__ = "Apache License 2.0"
16
- __version__ = "2.6.3"
16
+ __version__ = "0.0.6"
17
17
  __maintainer__ = "Alexander Willner"
18
18
  __email__ = "alex@willner.ws"
19
19
  __status__ = "Development"
20
20
 
21
21
  import datetime
22
22
  import os
23
+ import plistlib
23
24
  import sqlite3
24
25
  import sys
25
- from os import environ
26
26
 
27
27
 
28
28
  DEFAULT_DATABASE_FILEPATH = os.path.expanduser(
@@ -46,7 +46,8 @@ TYPE_TO_FILTER = {"to-do": "type = 0", "project": "type = 1", "heading": "type =
46
46
 
47
47
  INDICES = ("index", "todayIndex")
48
48
 
49
- COLUMNS_TO_OMIT_IF_NONE = ("area", "area_title", "project", "project_title", "heading", "heading_title", "trashed", "checklist", "tags")
49
+ COLUMNS_TO_OMIT_IF_NONE = ("area", "area_title", "project", "project_title",
50
+ "heading", "heading_title", "trashed", "checklist", "tags")
50
51
  COLUMNS_TO_TRANSFORM_TO_BOOL = ("trashed", "checklist", "tags")
51
52
 
52
53
 
@@ -100,7 +101,7 @@ class Database:
100
101
 
101
102
  # pylint: disable=R0913
102
103
  def __init__(self, filepath=None):
103
- self.filepath = filepath or environ.get("THINGSDB") or DEFAULT_DATABASE_FILEPATH
104
+ self.filepath = filepath or os.environ.get("THINGSDB") or DEFAULT_DATABASE_FILEPATH
104
105
 
105
106
  # Automated migration to new database location in Things 3.12.6/3.13.1
106
107
  # --------------------------------
@@ -114,22 +115,20 @@ class Database:
114
115
 
115
116
  # Core methods
116
117
 
117
- def get_tasks(
118
- self,
119
- uuid=None,
120
- type=None,
121
- status=None,
122
- start=None,
123
- area=None,
124
- project=None,
125
- heading=None,
126
- tag=None,
127
- start_date=None,
128
- deadline=None,
129
- index="index",
130
- count_only=False,
131
- search_query=None,
132
- ):
118
+ def get_tasks(self, # pylint: disable=R0914
119
+ uuid=None,
120
+ type=None, # pylint: disable=W0622
121
+ status=None,
122
+ start=None,
123
+ area=None,
124
+ project=None,
125
+ heading=None,
126
+ tag=None,
127
+ start_date=None,
128
+ deadline=None,
129
+ index="index",
130
+ count_only=False,
131
+ search_query=None):
133
132
  """Get tasks. See `api.tasks` for details on parameters."""
134
133
 
135
134
  # Overwrites
@@ -182,8 +181,8 @@ class Database:
182
181
  sql_query = self.make_task_sql_query(where_predicate)
183
182
  if count_only:
184
183
  return self.get_count(sql_query)
185
- else:
186
- return self.execute_query(sql_query)
184
+
185
+ return self.execute_query(sql_query)
187
186
 
188
187
  def make_task_sql_query(self, where_predicate):
189
188
  """Make SQL query for Task table"""
@@ -238,7 +237,9 @@ class Database:
238
237
  date(TASK.{self.DATE_DEADLINE}, "unixepoch") AS deadline,
239
238
  date(TASK.stopDate, "unixepoch") AS "stop_date",
240
239
  datetime(TASK.{self.DATE_CREATE}, "unixepoch", "localtime") AS created,
241
- datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified
240
+ datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified,
241
+ TASK.'index',
242
+ TASK.todayIndex
242
243
  FROM
243
244
  {self.TABLE_TASK} AS TASK
244
245
  LEFT OUTER JOIN
@@ -262,6 +263,7 @@ class Database:
262
263
  """
263
264
 
264
265
  def get_task_rows(self, where_predicate):
266
+ """Executes SQL query with given WHERE clauses."""
265
267
  return self.execute_query(self.make_task_sql_query(where_predicate))
266
268
 
267
269
  def get_areas(self, uuid=None, tag=None, count_only=False):
@@ -272,11 +274,7 @@ class Database:
272
274
  valid_tags = self.get_tags(titles_only=True)
273
275
  validate("tag", tag, [None] + list(valid_tags))
274
276
 
275
- if (
276
- uuid
277
- and count_only is False
278
- and not self.get_areas(uuid=uuid, count_only=True)
279
- ):
277
+ if (uuid and count_only is False and not self.get_areas(uuid=uuid, count_only=True)):
280
278
  raise ValueError(f"No such area uuid found: {uuid!r}")
281
279
 
282
280
  # Query
@@ -349,21 +347,21 @@ class Database:
349
347
  if titles_only:
350
348
  sql_query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
351
349
  return self.execute_query(sql_query, row_factory=list_factory)
352
- else:
353
- sql_query = f"""
354
- SELECT
355
- uuid,
356
- 'tag' AS type,
357
- title,
358
- shortcut
359
- FROM
360
- {self.TABLE_TAG}
361
- WHERE
362
- TRUE
363
- {make_filter('title', title)}
364
- ORDER BY "index"
365
- """
366
- return self.execute_query(sql_query)
350
+
351
+ sql_query = f"""
352
+ SELECT
353
+ uuid,
354
+ 'tag' AS type,
355
+ title,
356
+ shortcut
357
+ FROM
358
+ {self.TABLE_TAG}
359
+ WHERE
360
+ TRUE
361
+ {make_filter('title', title)}
362
+ ORDER BY "index"
363
+ """
364
+ return self.execute_query(sql_query)
367
365
 
368
366
  def get_tags_of_task(self, task_uuid):
369
367
  """Get tag titles of task"""
@@ -401,7 +399,6 @@ class Database:
401
399
 
402
400
  def get_version(self):
403
401
  """Get Things Database version."""
404
- import plistlib
405
402
 
406
403
  sql_query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
407
404
  result = self.execute_query(sql_query, row_factory=list_factory)
@@ -409,6 +406,7 @@ class Database:
409
406
  return plistlib.loads(plist_bytes)
410
407
 
411
408
  def get_count(self, sql):
409
+ """Count number of results."""
412
410
  sql_query = f"""SELECT COUNT(uuid) FROM ({sql})"""
413
411
  return self.execute_query(sql_query, row_factory=list_factory)[0]
414
412
 
@@ -420,7 +418,7 @@ class Database:
420
418
  print(sql_query)
421
419
  try:
422
420
  uri = f"file:{self.filepath}?mode=ro" # "ro" means read-only
423
- connection = sqlite3.connect(uri, uri=True)
421
+ connection = sqlite3.connect(uri, uri=True) # pylint: disable=E1101
424
422
  connection.row_factory = row_factory or dict_factory
425
423
  cursor = connection.cursor()
426
424
  cursor.execute(sql_query, parameters)
@@ -429,7 +427,7 @@ class Database:
429
427
  for task in tasks:
430
428
  print(task)
431
429
  return tasks
432
- except sqlite3.OperationalError as error:
430
+ except sqlite3.OperationalError as error: # pylint: disable=E1101
433
431
  print(f"Could not query the database at: {self.filepath}.")
434
432
  print(f"Details: {error}.")
435
433
  sys.exit(2)
@@ -437,10 +435,12 @@ class Database:
437
435
  # -------- Utility methods --------
438
436
 
439
437
  def last_modified(self):
438
+ """Get last modified time."""
440
439
  mtime_seconds = os.path.getmtime(self.filepath)
441
440
  return datetime.datetime.fromtimestamp(mtime_seconds)
442
441
 
443
442
  def was_modified_today(self):
443
+ """Was task modified today?"""
444
444
  last_modified_date = self.last_modified().date()
445
445
  todays_date = datetime.datetime.now().date()
446
446
  return last_modified_date >= todays_date
@@ -663,7 +663,7 @@ def dict_factory(cursor, row):
663
663
  return result
664
664
 
665
665
 
666
- def list_factory(cursor, row):
666
+ def list_factory(_cursor, row):
667
667
  """
668
668
  Convert SQL selects of one column into a list.
669
669
  """
@@ -671,6 +671,7 @@ def list_factory(cursor, row):
671
671
 
672
672
 
673
673
  def make_filter(column, value):
674
+ """Filter SQL query by AND {column} = "{value}"."""
674
675
  default = f'AND {column} = "{value}"'
675
676
  # special options
676
677
  return {