Skip to content

Commit 4a3e0e3

Browse files
committed
Add label search
Add search_labels methods (sync + async) using the new /labels/search endpoint.
1 parent 1287c44 commit 4a3e0e3

5 files changed

Lines changed: 104 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Support for searching projects
1313
- Support for searching sections
14+
- Support for searching labels
1415

1516
## [3.1.0] - 2025-05-07
1617

tests/test_api_labels.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,49 @@ async def test_get_labels(
8888
count += 1
8989

9090

91+
@pytest.mark.asyncio
92+
async def test_search_labels(
93+
todoist_api: TodoistAPI,
94+
todoist_api_async: TodoistAPIAsync,
95+
requests_mock: responses.RequestsMock,
96+
default_labels_response: list[PaginatedResults],
97+
default_labels_list: list[list[Label]],
98+
) -> None:
99+
endpoint = f"{DEFAULT_API_URL}/labels/search"
100+
query = "A label"
101+
102+
cursor: str | None = None
103+
for page in default_labels_response:
104+
requests_mock.add(
105+
method=responses.GET,
106+
url=endpoint,
107+
json=page,
108+
status=200,
109+
match=[
110+
auth_matcher(),
111+
request_id_matcher(),
112+
param_matcher({"query": query}, cursor),
113+
],
114+
)
115+
cursor = page["next_cursor"]
116+
117+
count = 0
118+
119+
labels_iter = todoist_api.search_labels(query)
120+
121+
for i, labels in enumerate(labels_iter):
122+
assert len(requests_mock.calls) == count + 1
123+
assert labels == default_labels_list[i]
124+
count += 1
125+
126+
labels_async_iter = await todoist_api_async.search_labels(query)
127+
128+
async for i, labels in enumerate_async(labels_async_iter):
129+
assert len(requests_mock.calls) == count + 1
130+
assert labels == default_labels_list[i]
131+
count += 1
132+
133+
91134
@pytest.mark.asyncio
92135
async def test_add_label_minimal(
93136
todoist_api: TodoistAPI,

todoist_api_python/_core/endpoints.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
SECTIONS_SEARCH_PATH_SUFFIX = "search"
2727
COMMENTS_PATH = "comments"
2828
LABELS_PATH = "labels"
29+
LABELS_SEARCH_PATH_SUFFIX = "search"
2930
SHARED_LABELS_PATH = "labels/shared"
3031
SHARED_LABELS_RENAME_PATH = f"{SHARED_LABELS_PATH}/rename"
3132
SHARED_LABELS_REMOVE_PATH = f"{SHARED_LABELS_PATH}/remove"

todoist_api_python/api.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
COLLABORATORS_PATH,
1313
COMMENTS_PATH,
1414
LABELS_PATH,
15+
LABELS_SEARCH_PATH_SUFFIX,
1516
PROJECT_ARCHIVE_PATH_SUFFIX,
1617
PROJECT_UNARCHIVE_PATH_SUFFIX,
1718
PROJECTS_PATH,
@@ -1330,7 +1331,7 @@ def get_labels(
13301331
Be aware that each iteration fires off a network request to the Todoist API,
13311332
and may result in rate limiting or other API restrictions.
13321333
1333-
:param limit: ` number of labels per page.
1334+
:param limit: Maximum number of labels per page.
13341335
:return: An iterable of lists of personal labels.
13351336
:raises requests.exceptions.HTTPError: If the API request fails.
13361337
:raises TypeError: If the API response structure is unexpected.
@@ -1351,6 +1352,41 @@ def get_labels(
13511352
params,
13521353
)
13531354

1355+
def search_labels(
1356+
self,
1357+
query: Annotated[str, MinLen(1), MaxLen(1024)],
1358+
*,
1359+
limit: Annotated[int, Ge(1), Le(200)] | None = None,
1360+
) -> Iterator[list[Label]]:
1361+
"""
1362+
Search personal labels by name.
1363+
1364+
The response is an iterable of lists of labels matching the query.
1365+
Be aware that each iteration fires off a network request to the Todoist API,
1366+
and may result in rate limiting or other API restrictions.
1367+
1368+
:param query: Query string for label names.
1369+
:param limit: Maximum number of labels per page.
1370+
:return: An iterable of lists of labels.
1371+
:raises requests.exceptions.HTTPError: If the API request fails.
1372+
:raises TypeError: If the API response structure is unexpected.
1373+
"""
1374+
endpoint = get_api_url(f"{LABELS_PATH}/{LABELS_SEARCH_PATH_SUFFIX}")
1375+
1376+
params: dict[str, Any] = {"query": query}
1377+
if limit is not None:
1378+
params["limit"] = limit
1379+
1380+
return ResultsPaginator(
1381+
self._session,
1382+
endpoint,
1383+
"results",
1384+
Label.from_dict,
1385+
self._token,
1386+
self._request_id_fn,
1387+
params,
1388+
)
1389+
13541390
def add_label(
13551391
self,
13561392
name: Annotated[str, MinLen(1), MaxLen(60)],

todoist_api_python/api_async.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,28 @@ async def get_labels(
894894
paginator = self._api.get_labels(limit=limit)
895895
return generate_async(paginator)
896896

897+
async def search_labels(
898+
self,
899+
query: Annotated[str, MinLen(1), MaxLen(1024)],
900+
*,
901+
limit: Annotated[int, Ge(1), Le(200)] | None = None,
902+
) -> AsyncGenerator[list[Label]]:
903+
"""
904+
Search personal labels by name.
905+
906+
The response is an iterable of lists of labels matching the query.
907+
Be aware that each iteration fires off a network request to the Todoist API,
908+
and may result in rate limiting or other API restrictions.
909+
910+
:param query: Query string for label names.
911+
:param limit: Maximum number of labels per page.
912+
:return: An iterable of lists of labels.
913+
:raises requests.exceptions.HTTPError: If the API request fails.
914+
:raises TypeError: If the API response structure is unexpected.
915+
"""
916+
paginator = self._api.search_labels(query, limit=limit)
917+
return generate_async(paginator)
918+
897919
async def add_label(
898920
self,
899921
name: Annotated[str, MinLen(1), MaxLen(60)],

0 commit comments

Comments
 (0)