From 5c4f5c64be9007afabeac1da2b354a1974c8d6da Mon Sep 17 00:00:00 2001 From: lemon24 Date: Mon, 2 Dec 2024 21:54:06 +0200 Subject: [PATCH] Allow filtering entries by the entry source. #267 --- CHANGES.rst | 1 + src/reader/_storage/_entries.py | 8 +++++- src/reader/_types.py | 5 ++++ src/reader/core.py | 44 +++++++++++++++++++++++++++------ tests/test_reader_filter.py | 6 ++++- 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8b917bfb..9394b0dc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,7 @@ Unreleased * Add :attr:`Entry.feed_resolved_title` and :attr:`Feed.resolved_title` properties. * The ``feed`` search column now indexes :attr:`Entry.feed_resolved_title`, instead of feed :attr:`~Feed.user_title` or :attr:`~Feed.title`. + * Allow filtering entries by the entry source. * Fix :meth:`~Reader.enable_search` / :meth:`~Reader.update_search` not working when the search database is missing but change tracking is enabled diff --git a/src/reader/_storage/_entries.py b/src/reader/_storage/_entries.py index 7c0085db..043f3d89 100644 --- a/src/reader/_storage/_entries.py +++ b/src/reader/_storage/_entries.py @@ -518,7 +518,9 @@ def entry_filter( query: Query, filter: EntryFilter, keyword: str = 'WHERE' ) -> dict[str, Any]: add = getattr(query, keyword) - feed_url, entry_id, read, important, has_enclosures, tags, feed_tags = filter + feed_url, entry_id, read, important, has_enclosures, source_url, tags, feed_tags = ( + filter + ) context = {} @@ -544,6 +546,10 @@ def entry_filter( """ ) + if source_url: + add("json_extract(entries.source, '$.url') = :source_url") + context['source_url'] = source_url + context.update(entry_tags_filter(query, tags, keyword=keyword)) context.update(feed_tags_filter(query, feed_tags, 'entries.feed', keyword=keyword)) diff --git a/src/reader/_types.py b/src/reader/_types.py index dbcdc068..379cf28a 100644 --- a/src/reader/_types.py +++ b/src/reader/_types.py @@ -514,6 +514,7 @@ class EntryFilter(NamedTuple): read: bool | None = None important: TristateFilter = 'any' has_enclosures: bool | None = None + source: str | None = None tags: TagFilter = () feed_tags: TagFilter = () @@ -525,6 +526,7 @@ def from_args( read: bool | None = None, important: TristateFilterInput = None, has_enclosures: bool | None = None, + source: FeedInput | None = None, tags: TagFilterInput = None, feed_tags: TagFilterInput = None, ) -> Self: @@ -543,6 +545,8 @@ def from_args( if has_enclosures not in (None, False, True): raise ValueError("has_enclosures should be one of (None, False, True)") + source_url = _feed_argument(source) if source is not None else None + tag_filter = tag_filter_argument(tags) feed_tag_filter = tag_filter_argument(feed_tags, 'feed_tags') @@ -552,6 +556,7 @@ def from_args( read, important_filter, has_enclosures, + source_url, tag_filter, feed_tag_filter, ) diff --git a/src/reader/core.py b/src/reader/core.py index 5b96030c..787c229a 100644 --- a/src/reader/core.py +++ b/src/reader/core.py @@ -1091,6 +1091,7 @@ def get_entries( read: bool | None = None, important: TristateFilterInput = None, has_enclosures: bool | None = None, + source: FeedInput | None = None, tags: TagFilterInput = None, feed_tags: TagFilterInput = None, sort: EntrySort = 'recent', @@ -1131,7 +1132,8 @@ def get_entries( .. versionadded:: 1.2 Args: - feed (str or tuple(str) or Feed or None): Only return the entries for this feed. + feed (str or tuple(str) or Feed or None): + Only return the entries for this feed. entry (tuple(str, str) or Entry or None): Only return the entry with this (feed URL, entry id) tuple. read (bool or None): Only return (un)read entries. @@ -1141,6 +1143,8 @@ def get_entries( :data:`~reader.types.TristateFilterInput` string filters. has_enclosures (bool or None): Only return entries that (don't) have enclosures. + source (str or tuple(str) or Feed or None): + Only return the entries for this source. tags (None or bool or list(str or bool or list(str or bool))): Only return entries matching these tags; see :data:`~reader.types.TagFilterInput` for details. @@ -1177,13 +1181,16 @@ def get_entries( .. versionadded:: 3.11 The ``tags`` keyword argument. + .. versionadded:: 3.16 + The ``source`` keyword argument. + """ # If we ever implement pagination, consider following the guidance in # https://specs.openstack.org/openstack/api-wg/guidelines/pagination_filter_sort.html filter = EntryFilter.from_args( - feed, entry, read, important, has_enclosures, tags, feed_tags + feed, entry, read, important, has_enclosures, source, tags, feed_tags ) if sort not in ('recent', 'random'): @@ -1252,13 +1259,15 @@ def get_entry_counts( read: bool | None = None, important: TristateFilterInput = None, has_enclosures: bool | None = None, + source: FeedInput | None = None, tags: TagFilterInput = None, feed_tags: TagFilterInput = None, ) -> EntryCounts: """Count all or some of the entries. Args: - feed (str or tuple(str) or Feed or None): Only count the entries for this feed. + feed (str or tuple(str) or Feed or None): + Only count the entries for this feed. entry (tuple(str, str) or Entry or None): Only count the entry with this (feed URL, entry id) tuple. read (bool or None): Only count (un)read entries. @@ -1268,6 +1277,8 @@ def get_entry_counts( :data:`~reader.types.TristateFilterInput` string filters. has_enclosures (bool or None): Only count entries that (don't) have enclosures. + source (str or tuple(str) or Feed or None): + Only count the entries for this source. tags (None or bool or list(str or bool or list(str or bool))): Only count entries matching these tags; see :data:`~reader.types.TagFilterInput` for details. @@ -1289,10 +1300,13 @@ def get_entry_counts( .. versionadded:: 3.11 The ``tags`` keyword argument. + .. versionadded:: 3.16 + The ``source`` keyword argument. + """ filter = EntryFilter.from_args( - feed, entry, read, important, has_enclosures, tags, feed_tags + feed, entry, read, important, has_enclosures, source, tags, feed_tags ) now = self._now() return self._storage.get_entry_counts(now, filter) @@ -1657,6 +1671,7 @@ def search_entries( read: bool | None = None, important: TristateFilterInput = None, has_enclosures: bool | None = None, + source: FeedInput | None = None, tags: TagFilterInput = None, feed_tags: TagFilterInput = None, sort: SearchSortOrder = 'relevant', @@ -1723,7 +1738,8 @@ def search_entries( Args: query (str): The search query. - feed (str or tuple(str) or Feed or None): Only search the entries for this feed. + feed (str or tuple(str) or Feed or None): + Only search the entries for this feed. entry (tuple(str, str) or Entry or None): Only search for the entry with this (feed URL, entry id) tuple. read (bool or None): Only search (un)read entries. @@ -1733,6 +1749,8 @@ def search_entries( :data:`~reader.types.TristateFilterInput` string filters. has_enclosures (bool or None): Only search entries that (don't) have enclosures. + source (str or tuple(str) or Feed or None): + Only search the entries for this source. tags (None or bool or list(str or bool or list(str or bool))): Only search entries matching these tags; see :data:`~reader.types.TagFilterInput` for details. @@ -1775,9 +1793,12 @@ def search_entries( .. versionadded:: 3.11 The ``tags`` keyword argument. + .. versionadded:: 3.16 + The ``source`` keyword argument. + """ filter = EntryFilter.from_args( - feed, entry, read, important, has_enclosures, tags, feed_tags + feed, entry, read, important, has_enclosures, source, tags, feed_tags ) if sort not in ('relevant', 'recent', 'random'): @@ -1803,6 +1824,7 @@ def search_entry_counts( read: bool | None = None, important: TristateFilterInput = None, has_enclosures: bool | None = None, + source: FeedInput | None = None, tags: TagFilterInput = None, feed_tags: TagFilterInput = None, ) -> EntrySearchCounts: @@ -1814,7 +1836,8 @@ def search_entry_counts( Args: query (str): The search query. - feed (str or tuple(str) or Feed or None): Only count the entries for this feed. + feed (str or tuple(str) or Feed or None): + Only count the entries for this feed. entry (tuple(str, str) or Entry or None): Only count the entry with this (feed URL, entry id) tuple. read (bool or None or str): @@ -1824,6 +1847,8 @@ def search_entry_counts( important (bool or None): Only count (un)important entries. has_enclosures (bool or None): Only count entries that (don't) have enclosures. + source (str or tuple(str) or Feed or None): + Only count the entries for this source. tags (None or bool or list(str or bool or list(str or bool))): Only count entries matching these tags; see :data:`~reader.types.TagFilterInput` for details. @@ -1851,10 +1876,13 @@ def search_entry_counts( .. versionadded:: 3.11 The ``tags`` keyword argument. + .. versionadded:: 3.16 + The ``source`` keyword argument. + """ filter = EntryFilter.from_args( - feed, entry, read, important, has_enclosures, tags, feed_tags + feed, entry, read, important, has_enclosures, source, tags, feed_tags ) now = self._now() return self._search.search_entry_counts(query, now, filter) diff --git a/tests/test_reader_filter.py b/tests/test_reader_filter.py index 088cc9b1..78da5974 100644 --- a/tests/test_reader_filter.py +++ b/tests/test_reader_filter.py @@ -3,6 +3,7 @@ from fakeparser import Parser from reader import Enclosure from reader import Entry +from reader import EntrySource from reader import Feed from reader import make_reader from reader_methods import get_feeds @@ -152,7 +153,7 @@ def reader_entries(): one_three = parser.entry(1, 3) # important one_four = parser.entry(1, 4, enclosures=[Enclosure('http://e2')]) two = parser.feed(2) - two_one = parser.entry(2, 1) + two_one = parser.entry(2, 1, source=EntrySource(url='source')) reader.add_feed(one.url) reader.add_feed(two.url) @@ -188,6 +189,8 @@ def reader_entries(): (dict(entry=('1', '1, 2')), {(1, 2)}), (dict(entry=Entry('1, 2', feed=Feed('1'))), {(1, 2)}), (dict(entry=('inexistent', 'also-inexistent')), set()), + (dict(source='source'), {(2, 1)}), + (dict(source=Feed('source')), {(2, 1)}), ], ) @rename_argument('reader', 'reader_entries') @@ -205,6 +208,7 @@ def test_entries(reader, get_entries, kwargs, expected): dict(has_enclosures=object()), dict(feed=object()), dict(entry=object()), + dict(source=object()), ], ) @rename_argument('reader', 'reader_entries')