things-py 0.0.2__tar.gz → 0.0.3__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.2
3
+ Version: 0.0.3
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.
@@ -14,7 +14,7 @@ Description: # Things Python API
14
14
  [![Coverage Status](https://codecov.io/gh/thingsapi/things.py/branch/main/graph/badge.svg?token=DBWGKAEYAP)](https://codecov.io/gh/thingsapi/things.py)
15
15
  [![GitHub Release](https://img.shields.io/github/v/release/thingsapi/things.py?sort=semver)](https://github.com/thingsapi/things.py/releases)
16
16
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
17
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/things?label=pypi%20downloads)](https://pypi.org/project/things/)
17
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/things.py?label=pypi%20downloads)](https://pypi.org/project/things.py/)
18
18
  [![GitHub Download Count](https://img.shields.io/github/downloads/thingsapi/things.py/total.svg)](https://github.com/thingsapi/things.py/releases)
19
19
  [![GitHub Issues](https://img.shields.io/github/issues/thingsapi/things.py)](https://github.com/thingsapi/things.py/issues)
20
20
  [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/quality/g/thingsapi/things.py)](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
@@ -6,7 +6,7 @@ A simple Python 3 library to read your [Things app](https://culturedcode.com/thi
6
6
  [![Coverage Status](https://codecov.io/gh/thingsapi/things.py/branch/main/graph/badge.svg?token=DBWGKAEYAP)](https://codecov.io/gh/thingsapi/things.py)
7
7
  [![GitHub Release](https://img.shields.io/github/v/release/thingsapi/things.py?sort=semver)](https://github.com/thingsapi/things.py/releases)
8
8
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
9
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/things?label=pypi%20downloads)](https://pypi.org/project/things/)
9
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/things.py?label=pypi%20downloads)](https://pypi.org/project/things.py/)
10
10
  [![GitHub Download Count](https://img.shields.io/github/downloads/thingsapi/things.py/total.svg)](https://github.com/thingsapi/things.py/releases)
11
11
  [![GitHub Issues](https://img.shields.io/github/issues/thingsapi/things.py)](https://github.com/thingsapi/things.py/issues)
12
12
  [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/quality/g/thingsapi/things.py)](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
@@ -14,16 +14,16 @@ def package_files(directory):
14
14
  return paths
15
15
 
16
16
 
17
- APP = ['']
17
+ APP = [""]
18
18
  APP_NAME = "Things"
19
19
  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.2"
24
- DATA_FILES = package_files('')
23
+ VERSION = "0.0.3"
24
+ DATA_FILES = package_files("")
25
25
  OPTIONS = {
26
- 'argv_emulation': False,
26
+ "argv_emulation": False,
27
27
  }
28
28
 
29
29
  with open("README.md", "r") as fh:
@@ -44,11 +44,11 @@ setup(
44
44
  "Programming Language :: Python :: 3",
45
45
  "License :: OSI Approved :: Apache Software License",
46
46
  "Operating System :: MacOS :: MacOS X",
47
- "Natural Language :: English"
47
+ "Natural Language :: English",
48
48
  ],
49
- python_requires='>=3.6',
49
+ python_requires=">=3.6",
50
50
  version=VERSION,
51
51
  data_files=DATA_FILES,
52
- options={'py2app': OPTIONS},
53
- setup_requires=['py2app'],
52
+ options={"py2app": OPTIONS},
53
+ setup_requires=["py2app"],
54
54
  )
@@ -16,9 +16,9 @@ class ThingsCase(unittest.TestCase):
16
16
 
17
17
  def test_search(self):
18
18
  """Test search."""
19
- tasks = things.search('wrong_query', **FILEPATH)
19
+ tasks = things.search("wrong_query", **FILEPATH)
20
20
  self.assertEqual(0, len(tasks))
21
- tasks = things.search('To-Do % Heading', **FILEPATH)
21
+ tasks = things.search("To-Do % Heading", **FILEPATH)
22
22
  self.assertEqual(1, len(tasks))
23
23
 
24
24
  def test_inbox(self):
@@ -31,9 +31,9 @@ class ThingsCase(unittest.TestCase):
31
31
  tasks = things.upcoming(**FILEPATH)
32
32
  self.assertEqual(2, len(tasks))
33
33
 
34
- def test_due(self):
35
- """Test due."""
36
- tasks = things.due(**FILEPATH)
34
+ def test_deadlines(self):
35
+ """Test deadlines."""
36
+ tasks = things.deadlines(**FILEPATH)
37
37
  self.assertEqual(1, len(tasks))
38
38
 
39
39
  def test_today(self):
@@ -5,7 +5,7 @@ from things.api import ( # noqa
5
5
  areas,
6
6
  canceled,
7
7
  completed,
8
- due,
8
+ deadlines,
9
9
  get,
10
10
  inbox,
11
11
  link,
@@ -85,11 +85,24 @@ def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
85
85
  If the argument is `True`, only include tasks _with_ a start date.
86
86
  If the argument is `None` (default), then include all tasks.
87
87
 
88
- due_date : bool or None, optional
89
- If the argument is `False`, only include tasks _without_ a due date.
90
- If the argument is `True`, only include tasks _with_ a due date.
88
+ deadline : bool or None, optional
89
+ If the argument is `False`, only include tasks _without_ a deadline.
90
+ If the argument is `True`, only include tasks _with_ a deadline.
91
91
  If the argument is `None` (default), then include all tasks.
92
92
 
93
+ search_query : str, optional
94
+ The string value is passed to the SQL LIKE operator. It can thus
95
+ include placeholders such as '%' and '_'. Per default, it is
96
+ wrapped in '% ... %'.
97
+
98
+ Currently titles and notes of to-dos, projects, headings, and areas
99
+ are taken into account.
100
+
101
+ If the query is `False`, then return items where titles or notes
102
+ are empty.
103
+
104
+ If the query `None`, then return all items.
105
+
93
106
  index : {'index', 'todayIndex'}, default 'index'
94
107
  Database field to order result by.
95
108
 
@@ -315,26 +328,35 @@ def tags(title=None, include_items=False, **kwargs):
315
328
  # Utility API functions derived from above
316
329
  # --------------------------------------------------
317
330
 
331
+
318
332
  def search(query: str, **kwargs) -> List[Dict]:
319
333
  """
320
- Search the database. This takes mainly notes and titles into account.
334
+ Search tasks in the database.
321
335
 
322
- Parameters
323
- ----------
324
- query : str
325
- The string with optional placeholders (%) to search for.
336
+ Currently any part of a title and note of a to-do, project,
337
+ heading, or area is matched.
326
338
 
327
- Returns
328
- -------
329
- list of dict
330
- Representing Things items.
339
+ See the `search_query` parameter of `api.tasks` for details.
331
340
 
332
341
  Examples
333
342
  --------
334
- >>> things.search('substring % within notes and titles')
335
- ...
343
+ >>> things.search('book')
344
+ [{'uuid': 'YrOmUnEXASmpq8ch6RsyPt',
345
+ 'type': 'to-do',
346
+ 'title': 'Book a hotel room',
347
+ ...},
348
+ {'uuid': 'KVHAxIIJ52a0h1RCbmg5D6',
349
+ 'type': 'to-do',
350
+ 'title': 'Book flights',
351
+ ...}]
352
+
353
+ >>> things.search('book%room')
354
+ [{'uuid': 'YrOmUnEXASmpq8ch6RsyPt',
355
+ 'type': 'to-do',
356
+ 'title': 'Book a hotel room',
357
+ ...}]
336
358
  """
337
- return tasks(querystr=query, **kwargs)
359
+ return tasks(search_query=query, **kwargs)
338
360
 
339
361
 
340
362
  def get(uuid, default=None, **kwargs):
@@ -405,8 +427,8 @@ def today(**kwargs):
405
427
 
406
428
  def upcoming(**kwargs):
407
429
  """
408
- Note: unscheduled tasks with a due date are not included here.
409
- See the `due` function instead.
430
+ Note: unscheduled tasks with a deadline are not included here.
431
+ See the `deadline` function instead.
410
432
  """
411
433
  return tasks(start_date=True, start="Someday", **kwargs)
412
434
 
@@ -444,9 +466,9 @@ def completed(**kwargs):
444
466
  return tasks(status="completed", **kwargs)
445
467
 
446
468
 
447
- def due(**kwargs):
448
- result = tasks(due_date=True, **kwargs)
449
- result.sort(key=lambda task: task["due_date"])
469
+ def deadlines(**kwargs):
470
+ result = tasks(deadline=True, **kwargs)
471
+ result.sort(key=lambda task: task["deadline"])
450
472
  return result
451
473
 
452
474
 
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- """Simple read-only API for Things."""
4
+ """Simple read-only API for Things.
5
+
6
+ We attempt to follow the names used in the Things app, URL Scheme,
7
+ and SQL database whenever possible.
8
+ """
5
9
 
6
10
  from __future__ import print_function
7
11
 
@@ -25,19 +29,19 @@ DEFAULT_DATABASE_FILEPATH = os.path.expanduser(
25
29
  "/Things Database.thingsdatabase/main.sqlite"
26
30
  )
27
31
 
28
- START_TO_QUERY = {
32
+ START_TO_FILTER = {
29
33
  "Inbox": "start = 0",
30
34
  "Anytime": "start = 1",
31
35
  "Someday": "start = 2",
32
36
  }
33
37
 
34
- STATUS_TO_QUERY = {
38
+ STATUS_TO_FILTER = {
35
39
  "incomplete": "status = 0",
36
40
  "canceled": "status = 2",
37
41
  "completed": "status = 3",
38
42
  }
39
43
 
40
- TYPE_TO_QUERY = {"to-do": "type = 0", "project": "type = 1", "heading": "type = 2"}
44
+ TYPE_TO_FILTER = {"to-do": "type = 0", "project": "type = 1", "heading": "type = 2"}
41
45
 
42
46
  INDICES = ("index", "todayIndex")
43
47
 
@@ -67,25 +71,25 @@ class Database:
67
71
  TABLE_META = "Meta"
68
72
  DATE_CREATE = "creationDate"
69
73
  DATE_MOD = "userModificationDate"
70
- DATE_DUE = "dueDate"
74
+ DATE_DEADLINE = "dueDate"
71
75
  DATE_START = "startDate"
72
76
  DATE_STOP = "stopDate"
73
- IS_INBOX = START_TO_QUERY["Inbox"]
74
- IS_ANYTIME = START_TO_QUERY["Anytime"]
75
- IS_SOMEDAY = START_TO_QUERY["Someday"]
77
+ IS_INBOX = START_TO_FILTER["Inbox"]
78
+ IS_ANYTIME = START_TO_FILTER["Anytime"]
79
+ IS_SOMEDAY = START_TO_FILTER["Someday"]
76
80
  IS_SCHEDULED = f"{DATE_START} IS NOT NULL"
77
81
  IS_NOT_SCHEDULED = f"{DATE_START} IS NULL"
78
- IS_DUE = f"{DATE_DUE} IS NOT NULL"
82
+ IS_DEADLINE = f"{DATE_DEADLINE} IS NOT NULL"
79
83
  IS_RECURRING = "recurrenceRule IS NOT NULL"
80
84
  IS_NOT_RECURRING = "recurrenceRule IS NULL"
81
- IS_TODO = TYPE_TO_QUERY["to-do"]
82
- IS_PROJECT = TYPE_TO_QUERY["project"]
83
- IS_HEADING = TYPE_TO_QUERY["heading"]
85
+ IS_TODO = TYPE_TO_FILTER["to-do"]
86
+ IS_PROJECT = TYPE_TO_FILTER["project"]
87
+ IS_HEADING = TYPE_TO_FILTER["heading"]
84
88
  IS_NOT_TRASHED = "trashed = 0"
85
89
  IS_TRASHED = "trashed = 1"
86
- IS_INCOMPLETE = STATUS_TO_QUERY["incomplete"]
87
- IS_CANCELED = STATUS_TO_QUERY["canceled"]
88
- IS_COMPLETED = STATUS_TO_QUERY["completed"]
90
+ IS_INCOMPLETE = STATUS_TO_FILTER["incomplete"]
91
+ IS_CANCELED = STATUS_TO_FILTER["canceled"]
92
+ IS_COMPLETED = STATUS_TO_FILTER["completed"]
89
93
  RECURRING_IS_NOT_PAUSED = "instanceCreationPaused = 0"
90
94
  RECURRING_HAS_NEXT_STARTDATE = "nextInstanceStartDate IS NOT NULL"
91
95
 
@@ -120,10 +124,10 @@ class Database:
120
124
  heading=None,
121
125
  tag=None,
122
126
  start_date=None,
123
- due_date=None,
127
+ deadline=None,
124
128
  index="index",
125
129
  count_only=False,
126
- querystr=None
130
+ search_query=None,
127
131
  ):
128
132
  """Get tasks. See `api.tasks` for details on parameters."""
129
133
 
@@ -131,9 +135,9 @@ class Database:
131
135
  start = start and start.title()
132
136
 
133
137
  # Validation
134
- validate("type", type, [None] + list(TYPE_TO_QUERY))
135
- validate("status", status, [None] + list(STATUS_TO_QUERY))
136
- validate("start", start, [None] + list(START_TO_QUERY))
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))
137
141
  validate("index", index, list(INDICES))
138
142
 
139
143
  if tag is not None:
@@ -149,39 +153,39 @@ class Database:
149
153
  # See: https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute
150
154
 
151
155
  if uuid:
152
- query = f"TRUE {make_filter('TASK.uuid', uuid)}"
156
+ where_predicate = f"TRUE {make_filter('TASK.uuid', uuid)}"
153
157
  else:
154
- start_query = START_TO_QUERY.get(start)
155
- status_query = STATUS_TO_QUERY.get(status)
156
- type_query = TYPE_TO_QUERY.get(type)
158
+ start_filter = START_TO_FILTER.get(start, "")
159
+ status_filter = STATUS_TO_FILTER.get(status, "")
160
+ type_filter = TYPE_TO_FILTER.get(type, "")
157
161
 
158
- query = f"""
162
+ where_predicate = f"""
159
163
  TASK.{self.IS_NOT_TRASHED}
160
- {f"AND TASK.{type_query}" if type_query else ""}
161
- {f"AND TASK.{start_query}" if start_query else ""}
162
- {f"AND TASK.{status_query}" if status_query else ""}
164
+ {type_filter and f"AND TASK.{type_filter}"}
165
+ {start_filter and f"AND TASK.{start_filter}"}
166
+ {status_filter and f"AND TASK.{status_filter}"}
163
167
  AND TASK.{self.IS_NOT_RECURRING}
164
168
  AND (PROJECT.title IS NULL OR PROJECT.{self.IS_NOT_TRASHED})
165
169
  AND (HEADPROJ.title IS NULL OR HEADPROJ.{self.IS_NOT_TRASHED})
166
- {make_search(querystr)}
167
170
  {make_filter('TASK.uuid', uuid)}
168
171
  {make_filter("TASK.area", area)}
169
172
  {make_filter("TASK.project", project)}
170
173
  {make_filter("TASK.actionGroup", heading)}
171
174
  {make_filter("TASK.startDate", start_date)}
172
- {make_filter("TASK.dueDate", due_date)}
175
+ {make_filter(f"TASK.{self.DATE_DEADLINE}", deadline)}
173
176
  {make_filter("TAG.title", tag)}
177
+ {make_search_filter(search_query)}
174
178
  ORDER BY TASK."{index}"
175
179
  """
176
180
 
177
- sql = self.make_tasks_sql(query)
181
+ sql_query = self.make_task_sql_query(where_predicate)
178
182
  if count_only:
179
- return self.get_count(sql)
183
+ return self.get_count(sql_query)
180
184
  else:
181
- return self.execute_query(sql)
185
+ return self.execute_query(sql_query)
182
186
 
183
- def make_tasks_sql(self, query):
184
- """Make SQL for tasks"""
187
+ def make_task_sql_query(self, where_predicate):
188
+ """Make SQL query for Task table"""
185
189
  return f"""
186
190
  SELECT DISTINCT
187
191
  TASK.uuid,
@@ -221,8 +225,8 @@ class Database:
221
225
  WHEN CHECKLIST_ITEM.uuid IS NOT NULL THEN 1
222
226
  END AS checklist,
223
227
  date(TASK.startDate, "unixepoch") AS start_date,
224
- date(TASK.dueDate, "unixepoch") AS due_date,
225
- date(TASK.stopDate, "unixepoch") AS stop_date,
228
+ date(TASK.{self.DATE_DEADLINE}, "unixepoch") AS deadline,
229
+ date(TASK.stopDate, "unixepoch") AS "stop_date",
226
230
  datetime(TASK.{self.DATE_CREATE}, "unixepoch", "localtime") AS created,
227
231
  datetime(TASK.{self.DATE_MOD}, "unixepoch", "localtime") AS modified
228
232
  FROM
@@ -244,11 +248,11 @@ class Database:
244
248
  ON CHECKLIST_ITEM.task = TASK.uuid
245
249
  WHERE
246
250
  {self.filter}
247
- {query}
251
+ {where_predicate}
248
252
  """
249
253
 
250
- def get_rows(self, query):
251
- return self.execute_query(self.make_tasks_sql(query))
254
+ def get_task_rows(self, where_predicate):
255
+ return self.execute_query(self.make_task_sql_query(where_predicate))
252
256
 
253
257
  def get_areas(self, uuid=None, tag=None, count_only=False):
254
258
  """Get areas. See `api.areas` for details on parameters."""
@@ -266,7 +270,7 @@ class Database:
266
270
  raise ValueError(f"No such area uuid found: {uuid!r}")
267
271
 
268
272
  # Query
269
- query = f"""
273
+ sql_query = f"""
270
274
  SELECT
271
275
  AREA.uuid,
272
276
  'area' as type,
@@ -287,13 +291,13 @@ class Database:
287
291
  ORDER BY AREA."index"
288
292
  """
289
293
  if count_only:
290
- return self.get_count(query)
294
+ return self.get_count(sql_query)
291
295
  else:
292
- return self.execute_query(query)
296
+ return self.execute_query(sql_query)
293
297
 
294
298
  def get_checklist_items(self, task_uuid=None):
295
299
  """Get checklist items."""
296
- query = f"""
300
+ sql_query = f"""
297
301
  SELECT
298
302
  CHECKLIST_ITEM.title,
299
303
  CASE
@@ -302,7 +306,7 @@ class Database:
302
306
  WHEN CHECKLIST_ITEM.{self.IS_CANCELED} THEN 'canceled'
303
307
  END AS status,
304
308
  date(CHECKLIST_ITEM.stopDate, "unixepoch") AS stop_date,
305
- 'checklist item' as type,
309
+ 'checklist-item' as type,
306
310
  CHECKLIST_ITEM.uuid,
307
311
  datetime(
308
312
  CHECKLIST_ITEM.{self.DATE_MOD}, "unixepoch", "localtime"
@@ -316,7 +320,7 @@ class Database:
316
320
  CHECKLIST_ITEM.task = ?
317
321
  ORDER BY CHECKLIST_ITEM."index"
318
322
  """
319
- return self.execute_query(query, (task_uuid,))
323
+ return self.execute_query(sql_query, (task_uuid,))
320
324
 
321
325
  def get_tags(self, title=None, area=None, task=None, titles_only=False):
322
326
  """Get tags. See `api.tags` for details on parameters."""
@@ -333,84 +337,83 @@ class Database:
333
337
  return self.get_tags_of_area(area)
334
338
 
335
339
  if titles_only:
336
- query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
337
- return self.execute_query(query, row_factory=list_factory)
340
+ sql_query = f'SELECT title FROM {self.TABLE_TAG} ORDER BY "index"'
341
+ return self.execute_query(sql_query, row_factory=list_factory)
338
342
  else:
339
- query = f"""
340
- SELECT
341
- uuid,
342
- 'tag' AS type,
343
- title,
344
- shortcut
345
- FROM
346
- {self.TABLE_TAG}
347
- WHERE
348
- TRUE
349
- {make_filter('title', title)}
350
- ORDER BY "index"
351
- """
352
- return self.execute_query(query)
353
-
354
- def get_tags_of_task(self, task_uuid):
355
- """Get tag titles of task"""
356
- query = f"""
343
+ sql_query = f"""
357
344
  SELECT
358
- TAG.title
345
+ uuid,
346
+ 'tag' AS type,
347
+ title,
348
+ shortcut
359
349
  FROM
360
- {self.TABLE_TASKTAG} AS TASK_TAG
361
- LEFT OUTER JOIN
362
- {self.TABLE_TAG} TAG ON TAG.uuid = TASK_TAG.tags
350
+ {self.TABLE_TAG}
363
351
  WHERE
364
- TASK_TAG.tasks = ?
365
- ORDER BY TAG."index"
352
+ TRUE
353
+ {make_filter('title', title)}
354
+ ORDER BY "index"
366
355
  """
356
+ return self.execute_query(sql_query)
357
+
358
+ def get_tags_of_task(self, task_uuid):
359
+ """Get tag titles of task"""
360
+ sql_query = f"""
361
+ SELECT
362
+ TAG.title
363
+ FROM
364
+ {self.TABLE_TASKTAG} AS TASK_TAG
365
+ LEFT OUTER JOIN
366
+ {self.TABLE_TAG} TAG ON TAG.uuid = TASK_TAG.tags
367
+ WHERE
368
+ TASK_TAG.tasks = ?
369
+ ORDER BY TAG."index"
370
+ """
367
371
  return self.execute_query(
368
- query, parameters=(task_uuid,), row_factory=list_factory
372
+ sql_query, parameters=(task_uuid,), row_factory=list_factory
369
373
  )
370
374
 
371
375
  def get_tags_of_area(self, area_uuid):
372
376
  """Get tag titles for area"""
373
- query = f"""
374
- SELECT
375
- AREA.title
376
- FROM
377
- {self.TABLE_AREATAG} AS AREA_TAG
378
- LEFT OUTER JOIN
379
- {self.TABLE_TAG} AREA ON AREA.uuid = AREA_TAG.tags
380
- WHERE
381
- AREA_TAG.areas = ?
382
- ORDER BY AREA."index"
383
- """
384
-
377
+ sql_query = f"""
378
+ SELECT
379
+ AREA.title
380
+ FROM
381
+ {self.TABLE_AREATAG} AS AREA_TAG
382
+ LEFT OUTER JOIN
383
+ {self.TABLE_TAG} AREA ON AREA.uuid = AREA_TAG.tags
384
+ WHERE
385
+ AREA_TAG.areas = ?
386
+ ORDER BY AREA."index"
387
+ """
385
388
  return self.execute_query(
386
- query, parameters=(area_uuid,), row_factory=list_factory
389
+ sql_query, parameters=(area_uuid,), row_factory=list_factory
387
390
  )
388
391
 
389
392
  def get_version(self):
390
393
  """Get Things Database version."""
391
394
  import plistlib
392
395
 
393
- query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
394
- result = self.execute_query(query, row_factory=list_factory)
396
+ sql_query = f"SELECT value FROM {self.TABLE_META} WHERE key = 'databaseVersion'"
397
+ result = self.execute_query(sql_query, row_factory=list_factory)
395
398
  plist_bytes = result[0].encode()
396
399
  return plistlib.loads(plist_bytes)
397
400
 
398
401
  def get_count(self, sql):
399
- sql = f"""SELECT COUNT(uuid) FROM ({sql})"""
400
- return self.execute_query(sql, row_factory=list_factory)[0]
402
+ sql_query = f"""SELECT COUNT(uuid) FROM ({sql})"""
403
+ return self.execute_query(sql_query, row_factory=list_factory)[0]
401
404
 
402
405
  # noqa todo: add type hinting for resutl (List[Tuple[str, Any]]?)
403
- def execute_query(self, sql, parameters=(), row_factory=None):
404
- """Run the actual query"""
406
+ def execute_query(self, sql_query, parameters=(), row_factory=None):
407
+ """Run the actual SQL query"""
405
408
  if self.debug is True:
406
409
  print(self.filepath)
407
- print(sql)
410
+ print(sql_query)
408
411
  try:
409
412
  uri = f"file:{self.filepath}?mode=ro" # "ro" means read-only
410
413
  connection = sqlite3.connect(uri, uri=True)
411
414
  connection.row_factory = row_factory or dict_factory
412
415
  cursor = connection.cursor()
413
- cursor.execute(sql, parameters)
416
+ cursor.execute(sql_query, parameters)
414
417
  tasks = cursor.fetchall()
415
418
  if self.debug:
416
419
  for task in tasks:
@@ -441,7 +444,7 @@ class Database:
441
444
  TASK.{self.IS_TODO}
442
445
  ORDER BY TASK.{self.DATE_STOP}
443
446
  """
444
- return self.get_rows(query)
447
+ return self.get_task_rows(query)
445
448
 
446
449
  def get_lint(self):
447
450
  """Get tasks that float around"""
@@ -454,7 +457,7 @@ class Database:
454
457
  TASK.area IS NULL AND
455
458
  TASK.actionGroup IS NULL
456
459
  """
457
- return self.get_rows(query)
460
+ return self.get_task_rows(query)
458
461
 
459
462
  def get_empty_projects(self):
460
463
  """Get projects that are empty"""
@@ -480,7 +483,7 @@ class Database:
480
483
  )
481
484
  ) = 0
482
485
  """
483
- return self.get_rows(query)
486
+ return self.get_task_rows(query)
484
487
 
485
488
  def get_largest_projects(self):
486
489
  """Get projects that are empty"""
@@ -667,33 +670,40 @@ def make_filter(column, value):
667
670
  }.get(value, default)
668
671
 
669
672
 
670
- def make_search(value: str) -> str:
671
- query = ""
673
+ def make_search_filter(query: str) -> str:
674
+ """
675
+ Example
676
+ -------
677
+ >>> make_search_filter('dinner')
678
+ 'AND (
679
+ TASK.title LIKE "%dinner%"
680
+ OR TASK.notes LIKE "%dinner%"
681
+ OR AREA.title LIKE "%dinner%"
682
+ )'
683
+ """
684
+ if not query:
685
+ return ""
686
+
672
687
  # noqa todo 'TMChecklistItem.title'
673
- sources = ['TASK.title', 'TASK.notes', 'AREA.title']
674
- for source in sources:
675
- subsearch = make_sub_search(source, value)
676
- if subsearch:
677
- query = query + subsearch + ' OR '
678
- if query.endswith(' OR '):
679
- query = query[:-4]
680
- if query != "":
681
- query = 'AND (' + query + ')'
682
- return query
683
-
684
-
685
- def make_sub_search(column, value):
686
- default = f'{column} LIKE "%{value}%"'
687
- return {
688
- None: "",
689
- False: f"{column} IS NULL",
690
- True: f"{column} IS NOT NULL",
691
- }.get(value, default)
688
+ columns = ["TASK.title", "TASK.notes", "AREA.title"]
689
+
690
+ sub_searches = (f'{column} LIKE "%{query}%"' for column in columns)
691
+
692
+ return f"AND ({' OR '.join(sub_searches)})"
692
693
 
693
694
 
694
695
  def validate(parameter, argument, valid_arguments):
695
696
  """
696
697
  For a given parameter, check if its argument type is valid.
698
+ If not, then raise ValueError.
699
+
700
+ Example
701
+ -------
702
+ >>> validate(
703
+ parameter='status',
704
+ argument='completed',
705
+ valid_arguments=['incomplete', 'completed']
706
+ )
697
707
  """
698
708
  if argument in valid_arguments:
699
709
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: things.py
3
- Version: 0.0.2
3
+ Version: 0.0.3
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.
@@ -14,7 +14,7 @@ Description: # Things Python API
14
14
  [![Coverage Status](https://codecov.io/gh/thingsapi/things.py/branch/main/graph/badge.svg?token=DBWGKAEYAP)](https://codecov.io/gh/thingsapi/things.py)
15
15
  [![GitHub Release](https://img.shields.io/github/v/release/thingsapi/things.py?sort=semver)](https://github.com/thingsapi/things.py/releases)
16
16
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
17
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/things?label=pypi%20downloads)](https://pypi.org/project/things/)
17
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/things.py?label=pypi%20downloads)](https://pypi.org/project/things.py/)
18
18
  [![GitHub Download Count](https://img.shields.io/github/downloads/thingsapi/things.py/total.svg)](https://github.com/thingsapi/things.py/releases)
19
19
  [![GitHub Issues](https://img.shields.io/github/issues/thingsapi/things.py)](https://github.com/thingsapi/things.py/issues)
20
20
  [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/quality/g/thingsapi/things.py)](https://scrutinizer-ci.com/g/thingsapi/things.py/?branch=master)
File without changes
File without changes