Skip to content

Commit 8f2d749

Browse files
Merge pull request #27 from AlexanderWillner/main
added api.search - addressing https://github.com/thingsapi/things.py/…
2 parents 95dd383 + 9da2ebd commit 8f2d749

File tree

9 files changed

+75
-4
lines changed

9 files changed

+75
-4
lines changed

.coveragerc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[report]
2+
3+
exclude_lines =
4+
if __name__ == .__main__.:
5+
6+
[run]
7+
relative_files = True
8+
9+
omit =
10+
setup.py
11+
*/__init__.py
12+
src/*_app.py
13+
tests/*
14+
*site-packages*

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ test: ## Test the code
3434

3535
.PHONY: doc
3636
doc: ## Document the code
37-
@$(PYDOC) $(SRC_CORE).things
37+
@$(PYDOC) $(SRC_CORE).api
3838

3939
.PHONY: clean
4040
clean: ## Cleanup
@@ -87,7 +87,7 @@ feedback: ## Give feedback
8787

8888
upload: clean ## Upload the code
8989
@$(PYTHON) setup.py sdist bdist_wheel
90-
@$(PYTHON) -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/things*
90+
@$(PYTHON) -m twine upload dist/things.py*
9191

9292
copy-db:
9393
@cp ~/Library/Group\ Containers/JLMPQHK86H.com.culturedcode.ThingsMac/Things\ Database.thingsdatabase/main.sqlite* tests/

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def package_files(directory):
3333
app=APP,
3434
author=AUTHOR,
3535
author_email=AUTHOR_MAIL,
36-
name="things",
36+
name="things.py",
3737
description=DESCRIPTON,
3838
long_description=LONG_DESRIPTION,
3939
long_description_content_type="text/markdown",

tests/main.sqlite-shm

0 Bytes
Binary file not shown.

tests/main.sqlite-wal

-40.3 KB
Binary file not shown.

tests/test_things.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
class ThingsCase(unittest.TestCase):
1515
"""Class documentation goes here."""
1616

17+
def test_search(self):
18+
"""Test search."""
19+
tasks = things.search('wrong_query', **FILEPATH)
20+
self.assertEqual(0, len(tasks))
21+
tasks = things.search('To-Do % Heading', **FILEPATH)
22+
self.assertEqual(1, len(tasks))
23+
1724
def test_inbox(self):
1825
"""Test inbox."""
1926
tasks = things.inbox(**FILEPATH)

things/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
link,
1212
logbook,
1313
projects,
14+
search,
1415
show,
1516
someday,
1617
tags,

things/api.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
from shlex import quote
99
import sys
10+
from typing import List, Dict
1011

1112
from .database import Database
1213

@@ -16,7 +17,7 @@
1617
# --------------------------------------------------
1718

1819

19-
def tasks(uuid=None, include_items=False, **kwargs):
20+
def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901
2021
"""
2122
Read tasks into dicts.
2223
@@ -314,6 +315,27 @@ def tags(title=None, include_items=False, **kwargs):
314315
# Utility API functions derived from above
315316
# --------------------------------------------------
316317

318+
def search(query: str, **kwargs) -> List[Dict]:
319+
"""
320+
Search the database. This takes mainly notes and titles into account.
321+
322+
Parameters
323+
----------
324+
query : str
325+
The string with optional placeholders (%) to search for.
326+
327+
Returns
328+
-------
329+
list of dict
330+
Representing Things items.
331+
332+
Examples
333+
--------
334+
>>> things.search('substring % within notes and titles')
335+
...
336+
"""
337+
return tasks(querystr=query, **kwargs)
338+
317339

318340
def get(uuid, default=None, **kwargs):
319341
"""

things/database.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def get_tasks(
123123
due_date=None,
124124
index="index",
125125
count_only=False,
126+
querystr=None
126127
):
127128
"""Get tasks. See `api.tasks` for details on parameters."""
128129

@@ -162,6 +163,7 @@ def get_tasks(
162163
AND TASK.{self.IS_NOT_RECURRING}
163164
AND (PROJECT.title IS NULL OR PROJECT.{self.IS_NOT_TRASHED})
164165
AND (HEADPROJ.title IS NULL OR HEADPROJ.{self.IS_NOT_TRASHED})
166+
{make_search(querystr)}
165167
{make_filter('TASK.uuid', uuid)}
166168
{make_filter("TASK.area", area)}
167169
{make_filter("TASK.project", project)}
@@ -397,6 +399,7 @@ def get_count(self, sql):
397399
sql = f"""SELECT COUNT(uuid) FROM ({sql})"""
398400
return self.execute_query(sql, row_factory=list_factory)[0]
399401

402+
# noqa todo: add type hinting for resutl (List[Tuple[str, Any]]?)
400403
def execute_query(self, sql, parameters=(), row_factory=None):
401404
"""Run the actual query"""
402405
if self.debug is True:
@@ -664,6 +667,30 @@ def make_filter(column, value):
664667
}.get(value, default)
665668

666669

670+
def make_search(value: str) -> str:
671+
query = ""
672+
# 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)
692+
693+
667694
def validate(parameter, argument, valid_arguments):
668695
"""
669696
For a given parameter, check if its argument type is valid.

0 commit comments

Comments
 (0)