things-py 0.0.4__tar.gz → 0.0.6__tar.gz

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.
@@ -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.
@@ -20,7 +20,7 @@ AUTHOR = "Alexander Willner, Michael B."
20
20
  AUTHOR_MAIL = "alex@willner.ws"
21
21
  DESCRIPTON = "A simple Python 3 library to read your Things app data."
22
22
  URL = "https://github.com/thingsapi/things.py"
23
- VERSION = "0.0.4"
23
+ VERSION = "0.0.6"
24
24
  DATA_FILES = package_files("")
25
25
  OPTIONS = {
26
26
  "argv_emulation": False,
@@ -24,12 +24,12 @@ class ThingsCase(unittest.TestCase):
24
24
  def test_inbox(self):
25
25
  """Test inbox."""
26
26
  tasks = things.inbox(**FILEPATH)
27
- self.assertEqual(1, len(tasks))
27
+ self.assertEqual(2, len(tasks))
28
28
 
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,12 +39,20 @@ 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
+
44
+ def test_checklist(self):
45
+ """Test checklist."""
46
+ tasks = things.checklist_items('3Eva4XFof6zWb9iSfYy4ej', **FILEPATH)
47
+ self.assertEqual(3, len(tasks))
48
+ tasks = things.checklist_items('K9bx7h1xCJdevvyWardZDq', **FILEPATH)
49
+ self.assertEqual(0, len(tasks))
43
50
 
44
51
  def test_anytime(self):
45
- """Test antime."""
52
+ """Test anytime."""
46
53
  tasks = things.anytime(**FILEPATH)
47
- self.assertEqual(8, len(tasks))
54
+ self.assertEqual(11, len(tasks))
55
+ self.assertTrue(any(task.pop('area_title', '') == 'Area 1' for task in tasks))
48
56
 
49
57
  def test_logbook(self):
50
58
  """Test logbook."""
@@ -78,22 +86,25 @@ class ThingsCase(unittest.TestCase):
78
86
  def test_todos(self):
79
87
  """Test all tasks."""
80
88
  tasks = things.todos(start="Anytime", **FILEPATH)
81
- self.assertEqual(5, len(tasks))
89
+ self.assertEqual(8, len(tasks))
82
90
  tasks = things.todos(start="Anytime", status="completed", **FILEPATH)
83
91
  self.assertEqual(6, len(tasks))
84
92
  tasks = things.todos(status="completed", **FILEPATH)
85
93
  self.assertEqual(10, len(tasks))
86
94
  tasks = things.todos(include_items=True, **FILEPATH)
87
- self.assertEqual(9, len(tasks))
95
+ self.assertEqual(12, len(tasks))
88
96
  with self.assertRaises(ValueError):
89
97
  things.todos(status="wrong_value", **FILEPATH)
90
98
  tasks = things.tasks("A2oPvtt4dXoypeoLc8uYzY", **FILEPATH)
91
- self.assertEqual(13, len(tasks))
99
+ self.assertEqual(16, len(tasks))
92
100
 
93
101
  def test_tags(self):
94
102
  """Test all tags."""
95
103
  tags = things.tags(**FILEPATH)
96
104
  self.assertEqual(5, len(tags))
105
+ tasks = things.tasks(tag='Errand', **FILEPATH)
106
+ self.assertEqual(1, len(tasks))
107
+
97
108
 
98
109
  def test_projects(self):
99
110
  """Test all projects."""
@@ -103,7 +114,7 @@ class ThingsCase(unittest.TestCase):
103
114
  def test_areas(self):
104
115
  """Test all test_areas."""
105
116
  test_areas = things.areas(**FILEPATH)
106
- self.assertEqual(1, len(test_areas))
117
+ self.assertEqual(3, len(test_areas))
107
118
 
108
119
  def test_database_version(self):
109
120
  """Test database version."""
@@ -4,6 +4,7 @@ from things.api import ( # noqa
4
4
  anytime,
5
5
  areas,
6
6
  canceled,
7
+ checklist_items,
7
8
  completed,
8
9
  deadlines,
9
10
  get,
@@ -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,10 +113,11 @@ 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.
116
+ If the environment variable THINGSDB is set, then that path will be used.
117
+ Otherwise, access the default database path.
115
118
 
116
- database : things.Database, optional
117
- Any valid `things.Database` object previously instantiated.
119
+ database : things.database.Database, optional
120
+ Any valid `things.database.Database` object previously instantiated.
118
121
 
119
122
  Returns
120
123
  -------
@@ -214,8 +217,8 @@ def areas(uuid=None, include_items=False, **kwargs):
214
217
  Any valid path of a SQLite database file generated by the Things app.
215
218
  If no path is provided, then access the default database path.
216
219
 
217
- database : things.Database, optional
218
- Any valid `things.Database` object previously instantiated.
220
+ database : things.database.Database, optional
221
+ Any valid `things.database.Database` object previously instantiated.
219
222
 
220
223
  Returns
221
224
  -------
@@ -284,8 +287,8 @@ def tags(title=None, include_items=False, **kwargs):
284
287
  Any valid path of a SQLite database file generated by the Things app.
285
288
  If no path is provided, then access the default database path.
286
289
 
287
- database : things.Database, optional
288
- Any valid `things.Database` object previously instantiated.
290
+ database : things.database.Database, optional
291
+ Any valid `things.database.Database` object previously instantiated.
289
292
 
290
293
  Returns
291
294
  -------
@@ -336,7 +339,7 @@ def search(query: str, **kwargs) -> List[Dict]:
336
339
  Currently any part of a title and note of a to-do, project,
337
340
  heading, or area is matched.
338
341
 
339
- See the `search_query` parameter of `api.tasks` for details.
342
+ See the `search_query` parameter of `things.api.tasks` for details.
340
343
 
341
344
  Examples
342
345
  --------
@@ -389,7 +392,7 @@ def todos(uuid=None, **kwargs):
389
392
  """
390
393
  Read to-dos into dicts.
391
394
 
392
- See `things.tasks` for details.
395
+ See `things.api.tasks` for details.
393
396
  """
394
397
  return tasks(uuid=uuid, type="to-do", **kwargs)
395
398
 
@@ -398,11 +401,24 @@ def projects(uuid=None, **kwargs):
398
401
  """
399
402
  Read projects into dicts.
400
403
 
401
- See `things.tasks` for details.
404
+ See `things.api.tasks` for details.
402
405
  """
403
406
  return tasks(uuid=uuid, type="project", **kwargs)
404
407
 
405
408
 
409
+ def checklist_items(uuid, **kwargs):
410
+ """
411
+ Read checklist items into dicts.
412
+ See `things.api.tasks` for details.
413
+
414
+ Returns
415
+ -------
416
+ list of dict
417
+ Representing checklist items. Or [] if none found.
418
+ """
419
+ return tasks(uuid=uuid, type="to-do", **kwargs).get("checklist", []) # type: ignore
420
+
421
+
406
422
  # Filter by collections in the Things app sidebar.
407
423
 
408
424
 
@@ -410,7 +426,7 @@ def inbox(**kwargs):
410
426
  """
411
427
  Read inbox into dicts.
412
428
 
413
- See `things.tasks` for details.
429
+ See `things.api.tasks` for details.
414
430
  """
415
431
  return tasks(start="Inbox", **kwargs)
416
432
 
@@ -452,7 +468,7 @@ def anytime(**kwargs):
452
468
  """
453
469
  Read anytime tasks into dicts.
454
470
 
455
- See `things.tasks` for details.
471
+ See `things.api.tasks` for details.
456
472
  """
457
473
  return tasks(start="Anytime", **kwargs)
458
474
 
@@ -461,7 +477,7 @@ def someday(**kwargs):
461
477
  """
462
478
  Read someday tasks into dicts.
463
479
 
464
- See `things.tasks` for details.
480
+ See `things.api.tasks` for details.
465
481
  """
466
482
  return tasks(start_date=False, start="Someday", **kwargs)
467
483
 
@@ -470,7 +486,7 @@ def logbook(**kwargs):
470
486
  """
471
487
  Read logbook tasks into dicts.
472
488
 
473
- See `things.tasks` for details.
489
+ See `things.api.tasks` for details.
474
490
  """
475
491
  result = [*canceled(**kwargs), *completed(**kwargs)]
476
492
  result.sort(key=lambda task: task["stop_date"], reverse=True)
@@ -484,7 +500,7 @@ def canceled(**kwargs):
484
500
  """
485
501
  Read canceled tasks into dicts.
486
502
 
487
- See `things.tasks` for details.
503
+ See `things.api.tasks` for details.
488
504
  """
489
505
  return tasks(status="canceled", **kwargs)
490
506
 
@@ -505,7 +521,7 @@ def deadlines(**kwargs):
505
521
  """
506
522
  Read tasks with deadlines into dicts.
507
523
 
508
- See `things.tasks` for details.
524
+ See `things.api.tasks` for details.
509
525
  """
510
526
  result = tasks(deadline=True, **kwargs)
511
527
  result.sort(key=lambda task: task["deadline"])
@@ -13,13 +13,14 @@ __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
26
 
@@ -45,7 +46,8 @@ TYPE_TO_FILTER = {"to-do": "type = 0", "project": "type = 1", "heading": "type =
45
46
 
46
47
  INDICES = ("index", "todayIndex")
47
48
 
48
- COLUMNS_TO_OMIT_IF_NONE = ("area", "project", "heading", "trashed", "checklist", "tags")
49
+ COLUMNS_TO_OMIT_IF_NONE = ("area", "area_title", "project", "project_title",
50
+ "heading", "heading_title", "trashed", "checklist", "tags")
49
51
  COLUMNS_TO_TRANSFORM_TO_BOOL = ("trashed", "checklist", "tags")
50
52
 
51
53
 
@@ -99,7 +101,7 @@ class Database:
99
101
 
100
102
  # pylint: disable=R0913
101
103
  def __init__(self, filepath=None):
102
- self.filepath = filepath or DEFAULT_DATABASE_FILEPATH
104
+ self.filepath = filepath or os.environ.get("THINGSDB") or DEFAULT_DATABASE_FILEPATH
103
105
 
104
106
  # Automated migration to new database location in Things 3.12.6/3.13.1
105
107
  # --------------------------------
@@ -113,31 +115,29 @@ class Database:
113
115
 
114
116
  # Core methods
115
117
 
116
- def get_tasks(
117
- self,
118
- uuid=None,
119
- type=None,
120
- status=None,
121
- start=None,
122
- area=None,
123
- project=None,
124
- heading=None,
125
- tag=None,
126
- start_date=None,
127
- deadline=None,
128
- index="index",
129
- count_only=False,
130
- search_query=None,
131
- ):
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):
132
132
  """Get tasks. See `api.tasks` for details on parameters."""
133
133
 
134
134
  # Overwrites
135
135
  start = start and start.title()
136
136
 
137
137
  # Validation
138
- validate("type", type, [None] + list(TYPE_TO_FILTER))
139
- validate("status", status, [None] + list(STATUS_TO_FILTER))
140
- validate("start", start, [None] + list(START_TO_FILTER))
138
+ validate("type", type, [None] + list(TYPE_TO_FILTER)) # type: ignore
139
+ validate("status", status, [None] + list(STATUS_TO_FILTER)) # type: ignore
140
+ validate("start", start, [None] + list(START_TO_FILTER)) # type: ignore
141
141
  validate("index", index, list(INDICES))
142
142
 
143
143
  if tag is not None:
@@ -181,8 +181,8 @@ class Database:
181
181
  sql_query = self.make_task_sql_query(where_predicate)
182
182
  if count_only:
183
183
  return self.get_count(sql_query)
184
- else:
185
- return self.execute_query(sql_query)
184
+
185
+ return self.execute_query(sql_query)
186
186
 
187
187
  def make_task_sql_query(self, where_predicate):
188
188
  """Make SQL query for Task table"""
@@ -206,12 +206,21 @@ class Database:
206
206
  CASE
207
207
  WHEN AREA.uuid IS NOT NULL THEN AREA.uuid
208
208
  END AS area,
209
+ CASE
210
+ WHEN AREA.uuid IS NOT NULL THEN AREA.title
211
+ END AS area_title,
209
212
  CASE
210
213
  WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.uuid
211
214
  END AS project,
215
+ CASE
216
+ WHEN PROJECT.uuid IS NOT NULL THEN PROJECT.title
217
+ END AS project_title,
212
218
  CASE
213
219
  WHEN HEADING.uuid IS NOT NULL THEN HEADING.uuid
214
220
  END AS heading,
221
+ CASE
222
+ WHEN HEADING.uuid IS NOT NULL THEN HEADING.title
223
+ END AS heading_title,
215
224
  TASK.notes,
216
225
  CASE
217
226
  WHEN TAG.uuid IS NOT NULL THEN 1
@@ -228,7 +237,9 @@ class Database:
228
237
  date(TASK.{self.DATE_DEADLINE}, "unixepoch") AS deadline,
229
238
  date(TASK.stopDate, "unixepoch") AS "stop_date",
230
239
  datetime(TASK.{self.DATE_CREATE}, "unixepoch", "localtime") AS created,
231
- 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
232
243
  FROM
233
244
  {self.TABLE_TASK} AS TASK
234
245
  LEFT OUTER JOIN
@@ -252,6 +263,7 @@ class Database:
252
263
  """
253
264
 
254
265
  def get_task_rows(self, where_predicate):
266
+ """Executes SQL query with given WHERE clauses."""
255
267
  return self.execute_query(self.make_task_sql_query(where_predicate))
256
268
 
257
269
  def get_areas(self, uuid=None, tag=None, count_only=False):
@@ -262,17 +274,13 @@ class Database:
262
274
  valid_tags = self.get_tags(titles_only=True)
263
275
  validate("tag", tag, [None] + list(valid_tags))
264
276
 
265
- if (
266
- uuid
267
- and count_only is False
268
- and not self.get_areas(uuid=uuid, count_only=True)
269
- ):
277
+ if (uuid and count_only is False and not self.get_areas(uuid=uuid, count_only=True)):
270
278
  raise ValueError(f"No such area uuid found: {uuid!r}")
271
279
 
272
280
  # Query
273
281
  sql_query = f"""
274
282
  SELECT
275
- AREA.uuid,
283
+ DISTINCT AREA.uuid,
276
284
  'area' as type,
277
285
  AREA.title,
278
286
  CASE
@@ -292,8 +300,8 @@ class Database:
292
300
  """
293
301
  if count_only:
294
302
  return self.get_count(sql_query)
295
- else:
296
- return self.execute_query(sql_query)
303
+
304
+ return self.execute_query(sql_query)
297
305
 
298
306
  def get_checklist_items(self, task_uuid=None):
299
307
  """Get checklist items."""
@@ -339,21 +347,21 @@ class Database:
339
347
  if titles_only:
340
348
  sql_query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
341
349
  return self.execute_query(sql_query, row_factory=list_factory)
342
- else:
343
- sql_query = f"""
344
- SELECT
345
- uuid,
346
- 'tag' AS type,
347
- title,
348
- shortcut
349
- FROM
350
- {self.TABLE_TAG}
351
- WHERE
352
- TRUE
353
- {make_filter('title', title)}
354
- ORDER BY "index"
355
- """
356
- 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)
357
365
 
358
366
  def get_tags_of_task(self, task_uuid):
359
367
  """Get tag titles of task"""
@@ -391,7 +399,6 @@ class Database:
391
399
 
392
400
  def get_version(self):
393
401
  """Get Things Database version."""
394
- import plistlib
395
402
 
396
403
  sql_query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
397
404
  result = self.execute_query(sql_query, row_factory=list_factory)
@@ -399,6 +406,7 @@ class Database:
399
406
  return plistlib.loads(plist_bytes)
400
407
 
401
408
  def get_count(self, sql):
409
+ """Count number of results."""
402
410
  sql_query = f"""SELECT COUNT(uuid) FROM ({sql})"""
403
411
  return self.execute_query(sql_query, row_factory=list_factory)[0]
404
412
 
@@ -410,7 +418,7 @@ class Database:
410
418
  print(sql_query)
411
419
  try:
412
420
  uri = f"file:{self.filepath}?mode=ro" # "ro" means read-only
413
- connection = sqlite3.connect(uri, uri=True)
421
+ connection = sqlite3.connect(uri, uri=True) # pylint: disable=E1101
414
422
  connection.row_factory = row_factory or dict_factory
415
423
  cursor = connection.cursor()
416
424
  cursor.execute(sql_query, parameters)
@@ -419,7 +427,7 @@ class Database:
419
427
  for task in tasks:
420
428
  print(task)
421
429
  return tasks
422
- except sqlite3.OperationalError as error:
430
+ except sqlite3.OperationalError as error: # pylint: disable=E1101
423
431
  print(f"Could not query the database at: {self.filepath}.")
424
432
  print(f"Details: {error}.")
425
433
  sys.exit(2)
@@ -427,10 +435,12 @@ class Database:
427
435
  # -------- Utility methods --------
428
436
 
429
437
  def last_modified(self):
438
+ """Get last modified time."""
430
439
  mtime_seconds = os.path.getmtime(self.filepath)
431
440
  return datetime.datetime.fromtimestamp(mtime_seconds)
432
441
 
433
442
  def was_modified_today(self):
443
+ """Was task modified today?"""
434
444
  last_modified_date = self.last_modified().date()
435
445
  todays_date = datetime.datetime.now().date()
436
446
  return last_modified_date >= todays_date
@@ -653,7 +663,7 @@ def dict_factory(cursor, row):
653
663
  return result
654
664
 
655
665
 
656
- def list_factory(cursor, row):
666
+ def list_factory(_cursor, row):
657
667
  """
658
668
  Convert SQL selects of one column into a list.
659
669
  """
@@ -661,6 +671,7 @@ def list_factory(cursor, row):
661
671
 
662
672
 
663
673
  def make_filter(column, value):
674
+ """Filter SQL query by AND {column} = "{value}"."""
664
675
  default = f'AND {column} = "{value}"'
665
676
  # special options
666
677
  return {
@@ -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.
File without changes
File without changes
File without changes