reader 3.17__py3-none-any.whl → 3.19.dev0__py3-none-any.whl
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.
- reader/__init__.py +4 -1
- reader/_parser/__init__.py +2 -7
- reader/_parser/_lazy.py +1 -2
- reader/_parser/requests/_lazy.py +1 -2
- reader/_plugins/cli_status.py +2 -2
- reader/_plugins/enclosure_tags.py +4 -2
- reader/_storage/_base.py +1 -1
- reader/_storage/_entries.py +6 -6
- reader/_storage/_feeds.py +6 -4
- reader/_storage/_search.py +12 -11
- reader/_storage/_sql_utils.py +2 -3
- reader/_storage/_sqlite_utils.py +15 -4
- reader/_types.py +3 -7
- reader/_update.py +1 -2
- reader/core.py +39 -87
- reader/exceptions.py +15 -81
- reader/types.py +74 -22
- {reader-3.17.dist-info → reader-3.19.dev0.dist-info}/METADATA +11 -40
- {reader-3.17.dist-info → reader-3.19.dev0.dist-info}/RECORD +22 -22
- {reader-3.17.dist-info → reader-3.19.dev0.dist-info}/WHEEL +1 -1
- {reader-3.17.dist-info → reader-3.19.dev0.dist-info/licenses}/LICENSE +0 -0
- {reader-3.17.dist-info → reader-3.19.dev0.dist-info}/top_level.txt +0 -0
reader/__init__.py
CHANGED
|
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
|
|
|
37
37
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
__version__ = '3.
|
|
40
|
+
__version__ = '3.19.dev0'
|
|
41
41
|
|
|
42
42
|
# isort: off
|
|
43
43
|
|
|
@@ -58,6 +58,9 @@ from .types import (
|
|
|
58
58
|
FeedCounts as FeedCounts,
|
|
59
59
|
EntryCounts as EntryCounts,
|
|
60
60
|
EntrySearchCounts as EntrySearchCounts,
|
|
61
|
+
FeedSort as FeedSort,
|
|
62
|
+
EntrySort as EntrySort,
|
|
63
|
+
EntrySearchSort as EntrySearchSort,
|
|
61
64
|
UpdateResult as UpdateResult,
|
|
62
65
|
UpdatedFeed as UpdatedFeed,
|
|
63
66
|
EntryUpdateStatus as EntryUpdateStatus,
|
reader/_parser/__init__.py
CHANGED
|
@@ -239,13 +239,9 @@ class NotModified(RetrieveError):
|
|
|
239
239
|
_default_message = "not modified"
|
|
240
240
|
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
class RetrieveResult(_namedtuple_compat, Generic[F, T, E]):
|
|
242
|
+
class RetrieveResult(NamedTuple, Generic[F, T, E]):
|
|
244
243
|
"""The result of retrieving a feed, regardless of the outcome."""
|
|
245
244
|
|
|
246
|
-
# should be a NamedTuple, but the typing one became generic only in 3.11,
|
|
247
|
-
# and we don't want to depend on typing_extensions at runtime
|
|
248
|
-
|
|
249
245
|
#: The feed (a :class:`FeedArgument`, usually a :class:`FeedForUpdate`).
|
|
250
246
|
feed: F
|
|
251
247
|
|
|
@@ -339,8 +335,7 @@ class FeedForUpdateRetrieverType(RetrieverType[T_co], Protocol): # pragma: no c
|
|
|
339
335
|
"""
|
|
340
336
|
|
|
341
337
|
|
|
342
|
-
|
|
343
|
-
class ParseResult(_namedtuple_compat, Generic[F, E]):
|
|
338
|
+
class ParseResult(NamedTuple, Generic[F, E]):
|
|
344
339
|
"""The result of retrieving and parsing a feed, regardless of the outcome."""
|
|
345
340
|
|
|
346
341
|
#: The feed (a :class:`FeedArgument`, usually a :class:`.FeedForUpdate`).
|
reader/_parser/_lazy.py
CHANGED
reader/_parser/requests/_lazy.py
CHANGED
|
@@ -4,6 +4,7 @@ from collections.abc import Sequence
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from dataclasses import field
|
|
6
6
|
from typing import Any
|
|
7
|
+
from typing import Self
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
from typing import TypeVar
|
|
9
10
|
|
|
@@ -14,8 +15,6 @@ from . import TimeoutType
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING: # pragma: no cover
|
|
17
|
-
from typing_extensions import Self
|
|
18
|
-
|
|
19
18
|
from . import Headers
|
|
20
19
|
from . import RequestHook
|
|
21
20
|
from . import ResponseHook
|
reader/_plugins/cli_status.py
CHANGED
|
@@ -73,7 +73,6 @@ def save_output(reader, now, config, command_path, output):
|
|
|
73
73
|
|
|
74
74
|
try:
|
|
75
75
|
old_content = reader.get_entry((feed_url, entry_id)).content[0].value
|
|
76
|
-
reader.delete_entry((feed_url, entry_id))
|
|
77
76
|
except EntryNotFoundError:
|
|
78
77
|
old_content = ''
|
|
79
78
|
|
|
@@ -91,7 +90,8 @@ def save_output(reader, now, config, command_path, output):
|
|
|
91
90
|
title=title,
|
|
92
91
|
updated=now,
|
|
93
92
|
content=[dict(type='text/plain', value=''.join(parts))],
|
|
94
|
-
)
|
|
93
|
+
),
|
|
94
|
+
overwrite=True,
|
|
95
95
|
)
|
|
96
96
|
reader.mark_entry_as_read((feed_url, entry_id))
|
|
97
97
|
|
|
@@ -38,6 +38,7 @@ from flask import request
|
|
|
38
38
|
from flask import Response
|
|
39
39
|
from flask import stream_with_context
|
|
40
40
|
from flask import url_for
|
|
41
|
+
from jinja2.filters import do_striptags as striptags
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
blueprint = Blueprint('enclosure_tags', __name__)
|
|
@@ -146,12 +147,13 @@ def enclosure_tags_filter(enclosure, entry, feed_tags):
|
|
|
146
147
|
|
|
147
148
|
args = {'url': enclosure.href, 'filename': filename}
|
|
148
149
|
if entry.title:
|
|
149
|
-
args['title'] = entry.title
|
|
150
|
+
args['title'] = striptags(entry.title)
|
|
150
151
|
if album := (entry.feed.user_title or entry.feed.title):
|
|
152
|
+
album = striptags(album)
|
|
151
153
|
args['album'] = album
|
|
152
154
|
args['artist'] = album
|
|
153
155
|
elif artist := (entry.author or entry.feed.author):
|
|
154
|
-
args['artist'] = artist
|
|
156
|
+
args['artist'] = striptags(artist)
|
|
155
157
|
|
|
156
158
|
for tag in feed_tags:
|
|
157
159
|
if 'podcast' in tag.lower():
|
reader/_storage/_base.py
CHANGED
|
@@ -41,7 +41,7 @@ if debug: # pragma: no cover
|
|
|
41
41
|
stmt = data.pop('stmt', None)
|
|
42
42
|
print('STORAGE_DEBUG', json.dumps(data), file=sys.stderr)
|
|
43
43
|
if stmt:
|
|
44
|
-
print(stmt)
|
|
44
|
+
print(stmt, file=sys.stderr)
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
wrap_exceptions = partial(_sqlite_utils.wrap_exceptions, StorageError)
|
reader/_storage/_entries.py
CHANGED
|
@@ -55,7 +55,7 @@ class EntriesMixin(StorageBase):
|
|
|
55
55
|
def get_entries(
|
|
56
56
|
self,
|
|
57
57
|
filter: EntryFilter = EntryFilter(), # noqa: B008
|
|
58
|
-
sort: EntrySort =
|
|
58
|
+
sort: EntrySort = EntrySort.RECENT,
|
|
59
59
|
limit: int | None = None,
|
|
60
60
|
starting_after: tuple[str, str] | None = None,
|
|
61
61
|
) -> Iterable[Entry]:
|
|
@@ -64,7 +64,7 @@ class EntriesMixin(StorageBase):
|
|
|
64
64
|
partial(get_entries_query, filter, sort),
|
|
65
65
|
row_factory=entry_factory,
|
|
66
66
|
)
|
|
67
|
-
if sort !=
|
|
67
|
+
if sort != EntrySort.RANDOM:
|
|
68
68
|
last = self.get_entry_last(sort, starting_after) if starting_after else None
|
|
69
69
|
return paginated_query(limit, last)
|
|
70
70
|
else:
|
|
@@ -571,7 +571,7 @@ RECENT_SORT_KEY = SortKey(
|
|
|
571
571
|
desc=True,
|
|
572
572
|
)
|
|
573
573
|
|
|
574
|
-
ENTRY_SORT_KEYS = {
|
|
574
|
+
ENTRY_SORT_KEYS = {EntrySort.RECENT: RECENT_SORT_KEY}
|
|
575
575
|
|
|
576
576
|
|
|
577
577
|
def entries_recent_sort(
|
|
@@ -597,9 +597,9 @@ def entries_random_sort(query: Query) -> None:
|
|
|
597
597
|
query.ORDER_BY("random()")
|
|
598
598
|
|
|
599
599
|
|
|
600
|
-
ENTRIES_SORT: dict[
|
|
601
|
-
|
|
602
|
-
|
|
600
|
+
ENTRIES_SORT: dict[EntrySort, Callable[[Query], None]] = {
|
|
601
|
+
EntrySort.RECENT: entries_recent_sort,
|
|
602
|
+
EntrySort.RANDOM: entries_random_sort,
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
|
reader/_storage/_feeds.py
CHANGED
|
@@ -107,7 +107,7 @@ class FeedsMixin(StorageBase):
|
|
|
107
107
|
def get_feeds(
|
|
108
108
|
self,
|
|
109
109
|
filter: FeedFilter = FeedFilter(), # noqa: B008
|
|
110
|
-
sort: FeedSort =
|
|
110
|
+
sort: FeedSort = FeedSort.TITLE,
|
|
111
111
|
limit: int | None = None,
|
|
112
112
|
starting_after: str | None = None,
|
|
113
113
|
) -> Iterable[Feed]:
|
|
@@ -335,8 +335,10 @@ def feed_factory(row: tuple[Any, ...]) -> Feed:
|
|
|
335
335
|
FEED_SORT_KEYS = {
|
|
336
336
|
# values must be non-null, see #203 for details.
|
|
337
337
|
# url at the end makes the order deterministic.
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
FeedSort.TITLE: SortKey(
|
|
339
|
+
("kinda_title", "lower(coalesce(user_title, title, ''))"), "url"
|
|
340
|
+
),
|
|
341
|
+
FeedSort.ADDED: SortKey("added", "url", desc=True),
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
|
|
@@ -358,7 +360,7 @@ def feed_filter(query: Query, filter: FeedFilter) -> dict[str, Any]:
|
|
|
358
360
|
if new is not None:
|
|
359
361
|
query.WHERE(f"last_retrieved is {'' if new else 'NOT'} NULL")
|
|
360
362
|
if update_after is not None:
|
|
361
|
-
query.WHERE("update_after is NULL or update_after <= :update_after")
|
|
363
|
+
query.WHERE("(update_after is NULL or update_after <= :update_after)")
|
|
362
364
|
context.update(update_after=adapt_datetime(update_after))
|
|
363
365
|
|
|
364
366
|
return context
|
reader/_storage/_search.py
CHANGED
|
@@ -28,8 +28,9 @@ from ..exceptions import SearchError
|
|
|
28
28
|
from ..exceptions import SearchNotEnabledError
|
|
29
29
|
from ..types import EntrySearchCounts
|
|
30
30
|
from ..types import EntrySearchResult
|
|
31
|
+
from ..types import EntrySearchSort
|
|
32
|
+
from ..types import EntrySort
|
|
31
33
|
from ..types import HighlightedString
|
|
32
|
-
from ..types import SearchSortOrder
|
|
33
34
|
from . import _entries
|
|
34
35
|
from . import _html_utils
|
|
35
36
|
from . import _sqlite_utils
|
|
@@ -425,7 +426,7 @@ class Search:
|
|
|
425
426
|
self,
|
|
426
427
|
query: str,
|
|
427
428
|
filter: EntryFilter = EntryFilter(), # noqa: B008
|
|
428
|
-
sort:
|
|
429
|
+
sort: EntrySearchSort = EntrySearchSort.RELEVANT,
|
|
429
430
|
limit: int | None = None,
|
|
430
431
|
starting_after: tuple[str, str] | None = None,
|
|
431
432
|
) -> Iterable[EntrySearchResult]:
|
|
@@ -457,13 +458,13 @@ class Search:
|
|
|
457
458
|
)
|
|
458
459
|
|
|
459
460
|
# TODO: dupe of at least Storage.get_entries(), maybe deduplicate
|
|
460
|
-
if sort !=
|
|
461
|
+
if sort != EntrySearchSort.RANDOM:
|
|
461
462
|
last = None
|
|
462
463
|
if starting_after:
|
|
463
|
-
if sort ==
|
|
464
|
+
if sort == EntrySearchSort.RELEVANT:
|
|
464
465
|
last = self.search_entry_last(query, starting_after)
|
|
465
|
-
elif sort ==
|
|
466
|
-
last = self.storage.get_entry_last(
|
|
466
|
+
elif sort == EntrySearchSort.RECENT:
|
|
467
|
+
last = self.storage.get_entry_last(EntrySort.RECENT, starting_after)
|
|
467
468
|
else:
|
|
468
469
|
assert False, "shouldn't get here" # noqa: B011; # pragma: no cover
|
|
469
470
|
|
|
@@ -527,7 +528,7 @@ class Search:
|
|
|
527
528
|
|
|
528
529
|
|
|
529
530
|
def make_search_entries_query(
|
|
530
|
-
filter: EntryFilter, sort:
|
|
531
|
+
filter: EntryFilter, sort: EntrySearchSort
|
|
531
532
|
) -> tuple[Query, dict[str, Any]]:
|
|
532
533
|
search = (
|
|
533
534
|
Query()
|
|
@@ -585,12 +586,12 @@ def relevant_sort(query: Query) -> None:
|
|
|
585
586
|
)
|
|
586
587
|
|
|
587
588
|
|
|
588
|
-
SEARCH_ENTRIES_SORT: dict[
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
SEARCH_ENTRIES_SORT: dict[EntrySearchSort, Callable[[Query], None]] = {
|
|
590
|
+
EntrySearchSort.RELEVANT: relevant_sort,
|
|
591
|
+
EntrySearchSort.RECENT: partial(
|
|
591
592
|
_entries.entries_recent_sort, keyword='HAVING', id_prefix='search._'
|
|
592
593
|
),
|
|
593
|
-
|
|
594
|
+
EntrySearchSort.RANDOM: _entries.entries_random_sort,
|
|
594
595
|
}
|
|
595
596
|
|
|
596
597
|
|
reader/_storage/_sql_utils.py
CHANGED
|
@@ -34,6 +34,7 @@ from collections.abc import Iterable
|
|
|
34
34
|
from dataclasses import dataclass
|
|
35
35
|
from typing import Any
|
|
36
36
|
from typing import NamedTuple
|
|
37
|
+
from typing import Self
|
|
37
38
|
from typing import TYPE_CHECKING
|
|
38
39
|
from typing import TypeVar
|
|
39
40
|
from typing import Union
|
|
@@ -42,8 +43,6 @@ from typing import Union
|
|
|
42
43
|
if TYPE_CHECKING: # pragma: no cover
|
|
43
44
|
import sqlite3
|
|
44
45
|
|
|
45
|
-
from typing_extensions import Self
|
|
46
|
-
|
|
47
46
|
|
|
48
47
|
_T = TypeVar('_T')
|
|
49
48
|
_U = TypeVar('_U')
|
|
@@ -197,7 +196,7 @@ class BaseQuery:
|
|
|
197
196
|
|
|
198
197
|
yield '\n'
|
|
199
198
|
|
|
200
|
-
_indent = functools.partial(textwrap.indent, prefix=' ')
|
|
199
|
+
_indent = staticmethod(functools.partial(textwrap.indent, prefix=' '))
|
|
201
200
|
|
|
202
201
|
|
|
203
202
|
if TYPE_CHECKING: # pragma: no cover
|
reader/_storage/_sqlite_utils.py
CHANGED
|
@@ -23,6 +23,7 @@ from dataclasses import dataclass
|
|
|
23
23
|
from datetime import datetime
|
|
24
24
|
from datetime import timezone
|
|
25
25
|
from typing import Any
|
|
26
|
+
from typing import cast
|
|
26
27
|
from typing import no_type_check
|
|
27
28
|
from typing import TypeVar
|
|
28
29
|
|
|
@@ -95,8 +96,17 @@ def wrap_exceptions(
|
|
|
95
96
|
can only be fixed by the user (e.g. access permission denied), and aren't
|
|
96
97
|
domain-related (those should have other custom exceptions).
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
We intentionally rely on the error message, because:
|
|
100
|
+
|
|
101
|
+
* the DB-API exceptions are somewhat fuzzy in their meaning
|
|
102
|
+
* we can't access the SQLite result code prior to Python 3.11
|
|
103
|
+
* even having the primary result code (added in 3.11):
|
|
104
|
+
* it's generic SQLITE_ERROR for a lot of errors we care about
|
|
105
|
+
("no such ...", "... already exists", invalid FTS5 queries)
|
|
106
|
+
* we need the table name so we don't accidentally shadow bugs
|
|
107
|
+
* we need the extended result code for the same reason
|
|
108
|
+
(e.g. SQLITE_CONSTRAINT vs SQLITE_CONSTRAINT_FOREIGNKEY)
|
|
109
|
+
* ProgrammingError usually does not have an error code
|
|
100
110
|
|
|
101
111
|
Full discussion at https://github.com/lemon24/reader/issues/21
|
|
102
112
|
|
|
@@ -462,8 +472,9 @@ class LocalConnectionFactory:
|
|
|
462
472
|
if not self._local.is_creating_thread:
|
|
463
473
|
self._other_threads = True
|
|
464
474
|
|
|
465
|
-
|
|
466
|
-
|
|
475
|
+
# TODO: remove needless cast (needed after mypy 1.16.0 update)
|
|
476
|
+
db = cast(sqlite3.Connection, sqlite3.connect(self.path, **self.kwargs))
|
|
477
|
+
self._local.db = db
|
|
467
478
|
self._local.call_count = 0
|
|
468
479
|
|
|
469
480
|
try:
|
reader/_types.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import NamedTuple
|
|
|
20
20
|
from typing import overload
|
|
21
21
|
from typing import Protocol
|
|
22
22
|
from typing import runtime_checkable
|
|
23
|
-
from typing import
|
|
23
|
+
from typing import Self
|
|
24
24
|
from typing import TypeVar
|
|
25
25
|
from typing import Union
|
|
26
26
|
|
|
@@ -40,6 +40,7 @@ from .types import EntryCounts
|
|
|
40
40
|
from .types import EntryInput
|
|
41
41
|
from .types import EntrySearchCounts
|
|
42
42
|
from .types import EntrySearchResult
|
|
43
|
+
from .types import EntrySearchSort
|
|
43
44
|
from .types import EntrySort
|
|
44
45
|
from .types import EntrySource
|
|
45
46
|
from .types import ExceptionInfo
|
|
@@ -51,15 +52,10 @@ from .types import JSONType
|
|
|
51
52
|
from .types import MISSING
|
|
52
53
|
from .types import MissingType
|
|
53
54
|
from .types import ResourceId
|
|
54
|
-
from .types import SearchSortOrder
|
|
55
55
|
from .types import TagFilterInput
|
|
56
56
|
from .types import TristateFilterInput
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
60
|
-
from typing_extensions import Self
|
|
61
|
-
|
|
62
|
-
|
|
63
59
|
log = logging.getLogger("reader")
|
|
64
60
|
|
|
65
61
|
# Private API
|
|
@@ -1268,7 +1264,7 @@ class SearchType(Protocol): # pragma: no cover
|
|
|
1268
1264
|
query: str,
|
|
1269
1265
|
/,
|
|
1270
1266
|
filter: EntryFilter,
|
|
1271
|
-
sort:
|
|
1267
|
+
sort: EntrySearchSort,
|
|
1272
1268
|
limit: int | None,
|
|
1273
1269
|
starting_after: tuple[str, str] | None,
|
|
1274
1270
|
) -> Iterable[EntrySearchResult]:
|
reader/_update.py
CHANGED
|
@@ -430,8 +430,7 @@ class Pipeline:
|
|
|
430
430
|
config: UpdateConfig,
|
|
431
431
|
result: ParseResult[FeedForUpdate, ParseError],
|
|
432
432
|
) -> tuple[str, UpdatedFeed | None | Exception]:
|
|
433
|
-
feed = result
|
|
434
|
-
value = result.value
|
|
433
|
+
feed, value, _ = result
|
|
435
434
|
|
|
436
435
|
# TODO: don't duplicate code from update()
|
|
437
436
|
# TODO: the feed tag value should come from get_feeds_for_update()
|
reader/core.py
CHANGED
|
@@ -15,6 +15,7 @@ from types import MappingProxyType
|
|
|
15
15
|
from typing import Any
|
|
16
16
|
from typing import Literal
|
|
17
17
|
from typing import overload
|
|
18
|
+
from typing import Self
|
|
18
19
|
from typing import TYPE_CHECKING
|
|
19
20
|
from typing import TypeVar
|
|
20
21
|
|
|
@@ -58,6 +59,7 @@ from .types import EntryCounts
|
|
|
58
59
|
from .types import EntryInput
|
|
59
60
|
from .types import EntrySearchCounts
|
|
60
61
|
from .types import EntrySearchResult
|
|
62
|
+
from .types import EntrySearchSort
|
|
61
63
|
from .types import EntrySort
|
|
62
64
|
from .types import EntryUpdateStatus
|
|
63
65
|
from .types import Feed
|
|
@@ -68,7 +70,6 @@ from .types import JSONType
|
|
|
68
70
|
from .types import MISSING
|
|
69
71
|
from .types import MissingType
|
|
70
72
|
from .types import ResourceInput
|
|
71
|
-
from .types import SearchSortOrder
|
|
72
73
|
from .types import TagFilterInput
|
|
73
74
|
from .types import TristateFilterInput
|
|
74
75
|
from .types import UpdatedFeed
|
|
@@ -76,8 +77,6 @@ from .types import UpdateResult
|
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
if TYPE_CHECKING: # pragma: no cover
|
|
79
|
-
from typing_extensions import Self
|
|
80
|
-
|
|
81
80
|
from ._parser import Parser
|
|
82
81
|
|
|
83
82
|
|
|
@@ -557,7 +556,7 @@ class Reader:
|
|
|
557
556
|
updates_enabled: bool | None = None,
|
|
558
557
|
new: bool | None = None,
|
|
559
558
|
scheduled: bool = False,
|
|
560
|
-
sort: FeedSort =
|
|
559
|
+
sort: FeedSort = FeedSort.TITLE,
|
|
561
560
|
limit: int | None = None,
|
|
562
561
|
starting_after: FeedInput | None = None,
|
|
563
562
|
) -> Iterable[Feed]:
|
|
@@ -576,9 +575,8 @@ class Reader:
|
|
|
576
575
|
/ have been updated before.
|
|
577
576
|
scheduled (bool):
|
|
578
577
|
Only return feeds scheduled to be updated.
|
|
579
|
-
sort (
|
|
580
|
-
:
|
|
581
|
-
insensitive; default), or ``'added'`` (last added first).
|
|
578
|
+
sort (FeedSort):
|
|
579
|
+
How to order feeds; see :class:`FeedSort` for details.
|
|
582
580
|
limit (int or None): A limit on the number of feeds to be returned;
|
|
583
581
|
by default, all feeds are returned.
|
|
584
582
|
starting_after (str or tuple(str) or Feed or None):
|
|
@@ -617,9 +615,7 @@ class Reader:
|
|
|
617
615
|
filter = FeedFilter.from_args(
|
|
618
616
|
self._now(), feed, tags, broken, updates_enabled, new, scheduled
|
|
619
617
|
)
|
|
620
|
-
|
|
621
|
-
if sort not in ('title', 'added'):
|
|
622
|
-
raise ValueError("sort should be one of ('title', 'added')")
|
|
618
|
+
sort = FeedSort(sort)
|
|
623
619
|
|
|
624
620
|
if limit is not None:
|
|
625
621
|
if not isinstance(limit, numbers.Integral) or limit < 1:
|
|
@@ -1095,43 +1091,12 @@ class Reader:
|
|
|
1095
1091
|
source: FeedInput | None = None,
|
|
1096
1092
|
tags: TagFilterInput = None,
|
|
1097
1093
|
feed_tags: TagFilterInput = None,
|
|
1098
|
-
sort: EntrySort =
|
|
1094
|
+
sort: EntrySort = EntrySort.RECENT,
|
|
1099
1095
|
limit: int | None = None,
|
|
1100
1096
|
starting_after: EntryInput | None = None,
|
|
1101
1097
|
) -> Iterable[Entry]:
|
|
1102
1098
|
"""Get all or some of the entries.
|
|
1103
1099
|
|
|
1104
|
-
Entries are sorted according to ``sort``. Possible values:
|
|
1105
|
-
|
|
1106
|
-
``'recent'``
|
|
1107
|
-
|
|
1108
|
-
Most recent first. That is:
|
|
1109
|
-
|
|
1110
|
-
* by published date for entries imported on the first update
|
|
1111
|
-
(if an entry does not have :attr:`~Entry.published`,
|
|
1112
|
-
:attr:`~Entry.updated` is used)
|
|
1113
|
-
* by added date for entries imported after that
|
|
1114
|
-
|
|
1115
|
-
This is to make sure newly imported entries appear at the top
|
|
1116
|
-
regardless of when the feed says they were published,
|
|
1117
|
-
while not having all the old entries at the top for new feeds.
|
|
1118
|
-
|
|
1119
|
-
.. note::
|
|
1120
|
-
|
|
1121
|
-
The algorithm for "recent" is a heuristic and may change over time.
|
|
1122
|
-
|
|
1123
|
-
.. versionchanged:: 3.1
|
|
1124
|
-
Sort entries by added date most of the time,
|
|
1125
|
-
with the exception of those imported on the first update.
|
|
1126
|
-
Previously, entries would be sorted by added
|
|
1127
|
-
only if they were published less than 7 days ago.
|
|
1128
|
-
|
|
1129
|
-
``'random'``
|
|
1130
|
-
|
|
1131
|
-
Random order (shuffled). At at most 256 entries will be returned.
|
|
1132
|
-
|
|
1133
|
-
.. versionadded:: 1.2
|
|
1134
|
-
|
|
1135
1100
|
Args:
|
|
1136
1101
|
feed (str or tuple(str) or Feed or None):
|
|
1137
1102
|
Only return the entries for this feed.
|
|
@@ -1152,13 +1117,13 @@ class Reader:
|
|
|
1152
1117
|
feed_tags (None or bool or list(str or bool or list(str or bool))):
|
|
1153
1118
|
Only return entries from feeds matching these tags;
|
|
1154
1119
|
see :data:`~reader.types.TagFilterInput` for details.
|
|
1155
|
-
sort (
|
|
1156
|
-
|
|
1120
|
+
sort (EntrySort):
|
|
1121
|
+
How to order entries; see :class:`EntrySort` for details.
|
|
1157
1122
|
limit (int or None): A limit on the number of entries to be returned;
|
|
1158
1123
|
by default, all entries are returned.
|
|
1159
1124
|
starting_after (tuple(str, str) or Entry or None):
|
|
1160
1125
|
Return entries after this entry; a cursor for use in pagination.
|
|
1161
|
-
Using ``starting_after`` with ``sort=
|
|
1126
|
+
Using ``starting_after`` with ``sort=RANDOM`` is not supported.
|
|
1162
1127
|
|
|
1163
1128
|
Yields:
|
|
1164
1129
|
:class:`Entry`: Sorted according to ``sort``.
|
|
@@ -1193,17 +1158,15 @@ class Reader:
|
|
|
1193
1158
|
filter = EntryFilter.from_args(
|
|
1194
1159
|
feed, entry, read, important, has_enclosures, source, tags, feed_tags
|
|
1195
1160
|
)
|
|
1196
|
-
|
|
1197
|
-
if sort not in ('recent', 'random'):
|
|
1198
|
-
raise ValueError("sort should be one of ('recent', 'random')")
|
|
1161
|
+
sort = EntrySort(sort)
|
|
1199
1162
|
|
|
1200
1163
|
if limit is not None:
|
|
1201
1164
|
if not isinstance(limit, numbers.Integral) or limit < 1:
|
|
1202
1165
|
raise ValueError("limit should be a positive integer")
|
|
1203
1166
|
starting_after = _entry_argument(starting_after) if starting_after else None
|
|
1204
1167
|
|
|
1205
|
-
if starting_after and sort ==
|
|
1206
|
-
raise ValueError("using starting_after with sort=
|
|
1168
|
+
if starting_after and sort == EntrySort.RANDOM:
|
|
1169
|
+
raise ValueError("using starting_after with sort=RANDOM not supported")
|
|
1207
1170
|
|
|
1208
1171
|
return self._storage.get_entries(filter, sort, limit, starting_after)
|
|
1209
1172
|
|
|
@@ -1495,7 +1458,7 @@ class Reader:
|
|
|
1495
1458
|
"""
|
|
1496
1459
|
return self.set_entry_important(entry, False)
|
|
1497
1460
|
|
|
1498
|
-
def add_entry(self, entry: Any,
|
|
1461
|
+
def add_entry(self, entry: Any, /, *, overwrite: bool = False) -> None:
|
|
1499
1462
|
"""Add a new entry to an existing feed.
|
|
1500
1463
|
|
|
1501
1464
|
``entry`` can be any :class:`Entry`-like object,
|
|
@@ -1537,6 +1500,9 @@ class Reader:
|
|
|
1537
1500
|
|
|
1538
1501
|
Args:
|
|
1539
1502
|
entry (Entry or dict): An entry-like object or equivalent mapping.
|
|
1503
|
+
overwrite (bool):
|
|
1504
|
+
If true and the entry already exists,
|
|
1505
|
+
overwrite it instead of raising :exc:`EntryExistsError`.
|
|
1540
1506
|
|
|
1541
1507
|
Raises:
|
|
1542
1508
|
EntryExistsError: If an entry with the same id already exists.
|
|
@@ -1551,6 +1517,9 @@ class Reader:
|
|
|
1551
1517
|
.. versionchanged:: 3.16
|
|
1552
1518
|
Allow setting :attr:`~Entry.source`.
|
|
1553
1519
|
|
|
1520
|
+
.. versionadded:: 3.18
|
|
1521
|
+
The ``overwrite`` argument.
|
|
1522
|
+
|
|
1554
1523
|
"""
|
|
1555
1524
|
|
|
1556
1525
|
# `entry` is of type Union[EntryDataLikeProtocol, EntryDataTypedDict],
|
|
@@ -1570,9 +1539,19 @@ class Reader:
|
|
|
1570
1539
|
added_by='user',
|
|
1571
1540
|
)
|
|
1572
1541
|
|
|
1573
|
-
|
|
1542
|
+
status = EntryUpdateStatus.NEW
|
|
1543
|
+
if overwrite:
|
|
1544
|
+
(old_entry,) = self._storage.get_entries_for_update(
|
|
1545
|
+
[entry_data.resource_id]
|
|
1546
|
+
)
|
|
1547
|
+
if old_entry:
|
|
1548
|
+
status = EntryUpdateStatus.MODIFIED
|
|
1549
|
+
self._storage.add_or_update_entries([intent])
|
|
1550
|
+
else:
|
|
1551
|
+
self._storage.add_entry(intent)
|
|
1552
|
+
|
|
1574
1553
|
for entry_hook in self.after_entry_update_hooks:
|
|
1575
|
-
entry_hook(self, intent.entry,
|
|
1554
|
+
entry_hook(self, intent.entry, status)
|
|
1576
1555
|
|
|
1577
1556
|
def delete_entry(self, entry: EntryInput, /, missing_ok: bool = False) -> None:
|
|
1578
1557
|
"""Delete an entry.
|
|
@@ -1732,37 +1711,12 @@ class Reader:
|
|
|
1732
1711
|
source: FeedInput | None = None,
|
|
1733
1712
|
tags: TagFilterInput = None,
|
|
1734
1713
|
feed_tags: TagFilterInput = None,
|
|
1735
|
-
sort:
|
|
1714
|
+
sort: EntrySearchSort = EntrySearchSort.RELEVANT,
|
|
1736
1715
|
limit: int | None = None,
|
|
1737
1716
|
starting_after: EntryInput | None = None,
|
|
1738
1717
|
) -> Iterable[EntrySearchResult]:
|
|
1739
1718
|
"""Get entries matching a full-text search query.
|
|
1740
1719
|
|
|
1741
|
-
Entries are sorted according to ``sort``. Possible values:
|
|
1742
|
-
|
|
1743
|
-
``'relevant'``
|
|
1744
|
-
|
|
1745
|
-
Most relevant first.
|
|
1746
|
-
|
|
1747
|
-
``'recent'``
|
|
1748
|
-
|
|
1749
|
-
Most recent first. See :meth:`~Reader.get_entries()`
|
|
1750
|
-
for details on what *recent* means.
|
|
1751
|
-
|
|
1752
|
-
.. versionadded:: 1.4
|
|
1753
|
-
|
|
1754
|
-
.. versionchanged:: 3.1
|
|
1755
|
-
Sort entries by added date most of the time,
|
|
1756
|
-
with the exception of those imported on the first update.
|
|
1757
|
-
Previously, entries would be sorted by added
|
|
1758
|
-
only if they were published less than 7 days ago.
|
|
1759
|
-
|
|
1760
|
-
``'random'``
|
|
1761
|
-
|
|
1762
|
-
Random order (shuffled). At at most 256 entries will be returned.
|
|
1763
|
-
|
|
1764
|
-
.. versionadded:: 1.10
|
|
1765
|
-
|
|
1766
1720
|
Note:
|
|
1767
1721
|
The query syntax is dependent on the search provider.
|
|
1768
1722
|
|
|
@@ -1815,13 +1769,13 @@ class Reader:
|
|
|
1815
1769
|
feed_tags (None or bool or list(str or bool or list(str or bool))):
|
|
1816
1770
|
Only search entries from feeds matching these tags;
|
|
1817
1771
|
see :data:`~reader.types.TagFilterInput` for details.
|
|
1818
|
-
sort (
|
|
1819
|
-
|
|
1772
|
+
sort (EntrySearchSort):
|
|
1773
|
+
How to order results; see :class:`EntrySearchSort` for details.
|
|
1820
1774
|
limit (int or None): A limit on the number of results to be returned;
|
|
1821
1775
|
by default, all results are returned.
|
|
1822
1776
|
starting_after (tuple(str, str) or EntrySearchResult or None):
|
|
1823
1777
|
Return results after this result; a cursor for use in pagination.
|
|
1824
|
-
Using ``starting_after`` with ``sort=
|
|
1778
|
+
Using ``starting_after`` with ``sort=RANDOM`` is not supported.
|
|
1825
1779
|
|
|
1826
1780
|
Yields:
|
|
1827
1781
|
:class:`EntrySearchResult`: Sorted according to ``sort``.
|
|
@@ -1858,17 +1812,15 @@ class Reader:
|
|
|
1858
1812
|
filter = EntryFilter.from_args(
|
|
1859
1813
|
feed, entry, read, important, has_enclosures, source, tags, feed_tags
|
|
1860
1814
|
)
|
|
1861
|
-
|
|
1862
|
-
if sort not in ('relevant', 'recent', 'random'):
|
|
1863
|
-
raise ValueError("sort should be one of ('relevant', 'recent', 'random')")
|
|
1815
|
+
sort = EntrySearchSort(sort)
|
|
1864
1816
|
|
|
1865
1817
|
if limit is not None:
|
|
1866
1818
|
if not isinstance(limit, numbers.Integral) or limit < 1:
|
|
1867
1819
|
raise ValueError("limit should be a positive integer")
|
|
1868
1820
|
starting_after = _entry_argument(starting_after) if starting_after else None
|
|
1869
1821
|
|
|
1870
|
-
if starting_after and sort ==
|
|
1871
|
-
raise ValueError("using starting_after with sort=
|
|
1822
|
+
if starting_after and sort == EntrySearchSort.RANDOM:
|
|
1823
|
+
raise ValueError("using starting_after with sort=RANDOM not supported")
|
|
1872
1824
|
|
|
1873
1825
|
return self._search.search_entries(query, filter, sort, limit, starting_after)
|
|
1874
1826
|
|
reader/exceptions.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterable
|
|
4
3
|
from collections.abc import Sequence
|
|
5
4
|
from functools import cached_property
|
|
6
|
-
from traceback import format_exception
|
|
7
5
|
from typing import Any
|
|
6
|
+
from typing import TypeVar
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class _FancyExceptionBase(Exception):
|
|
@@ -63,79 +62,6 @@ class _FancyExceptionBase(Exception):
|
|
|
63
62
|
return ': '.join(filter(None, parts))
|
|
64
63
|
|
|
65
64
|
|
|
66
|
-
class _ExceptionGroup(Exception): # pragma: no cover
|
|
67
|
-
"""ExceptionGroup shim for Python 3.10.
|
|
68
|
-
|
|
69
|
-
Always includes a traceback in str(exc),
|
|
70
|
-
so no traceback.* monkeypatching is needed,
|
|
71
|
-
at the expense of the traceback not being in the right place,
|
|
72
|
-
and a slightly non-standard traceback.
|
|
73
|
-
|
|
74
|
-
Avoids dependency on https://pypi.org/project/exceptiongroup/
|
|
75
|
-
|
|
76
|
-
>>> raise _ExceptionGroup('message', [
|
|
77
|
-
... NameError('one'),
|
|
78
|
-
... _ExceptionGroup('another', [
|
|
79
|
-
... AttributeError('two'),
|
|
80
|
-
... ]),
|
|
81
|
-
... ])
|
|
82
|
-
Traceback (most recent call last):
|
|
83
|
-
File "<stdin>", line 1, in <module>
|
|
84
|
-
reader.exceptions._ExceptionGroup: message (2 sub-exceptions)
|
|
85
|
-
+---------------- 1 -----------------
|
|
86
|
-
| NameError: one
|
|
87
|
-
+---------------- 2 -----------------
|
|
88
|
-
| reader.exceptions._ExceptionGroup: another (1 sub-exception)
|
|
89
|
-
| +---------------- 1 -----------------
|
|
90
|
-
| | AttributeError: two
|
|
91
|
-
| +------------------------------------
|
|
92
|
-
+------------------------------------
|
|
93
|
-
|
|
94
|
-
TODO: Remove this once we drop Python 3.10.
|
|
95
|
-
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def __init__(self, msg: str, excs: Sequence[Exception], /):
|
|
99
|
-
if not isinstance(msg, str): # pragma: no cover
|
|
100
|
-
raise TypeError(f"first argument must be str, not {type(msg).__name__}")
|
|
101
|
-
excs = tuple(excs)
|
|
102
|
-
if not excs: # pragma: no cover
|
|
103
|
-
raise ValueError("second argument must be a non-empty sequence")
|
|
104
|
-
for i, e in enumerate(excs): # pragma: no cover
|
|
105
|
-
if not isinstance(e, Exception):
|
|
106
|
-
raise ValueError(f"item {i} of second argument is not an exception")
|
|
107
|
-
self._message = msg
|
|
108
|
-
self._exceptions = excs
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def message(self) -> str:
|
|
112
|
-
return self._message
|
|
113
|
-
|
|
114
|
-
@property
|
|
115
|
-
def exceptions(self) -> tuple[Exception, ...]:
|
|
116
|
-
return self._exceptions
|
|
117
|
-
|
|
118
|
-
def _format_lines(self) -> Iterable[str]:
|
|
119
|
-
count = len(self.exceptions)
|
|
120
|
-
s = 's' if count != 1 else ''
|
|
121
|
-
yield f"{self.message} ({count} sub-exception{s})\n"
|
|
122
|
-
for i, exc in enumerate(self.exceptions, 1):
|
|
123
|
-
yield f"+{f' {i} '.center(36, '-')}\n"
|
|
124
|
-
for line in format_exception(exc):
|
|
125
|
-
for subline in line.rstrip().splitlines():
|
|
126
|
-
yield f"| {subline}\n"
|
|
127
|
-
yield f"+{'-' * 36}\n"
|
|
128
|
-
|
|
129
|
-
def __str__(self) -> str:
|
|
130
|
-
return ''.join(self._format_lines()).rstrip()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
ExceptionGroup # type: ignore[used-before-def,unused-ignore]
|
|
135
|
-
except NameError: # pragma: no cover
|
|
136
|
-
ExceptionGroup = _ExceptionGroup
|
|
137
|
-
|
|
138
|
-
|
|
139
65
|
class ReaderError(_FancyExceptionBase):
|
|
140
66
|
"""Base for all public exceptions."""
|
|
141
67
|
|
|
@@ -351,21 +277,29 @@ class SingleUpdateHookError(UpdateHookError):
|
|
|
351
277
|
return ': '.join(parts)
|
|
352
278
|
|
|
353
279
|
|
|
354
|
-
|
|
280
|
+
_UpdateHookErrorT = TypeVar('_UpdateHookErrorT', bound=UpdateHookError)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class UpdateHookErrorGroup(ExceptionGroup[_UpdateHookErrorT], UpdateHookError):
|
|
355
284
|
r"""A (possibly nested) :exc:`ExceptionGroup` of :exc:`UpdateHookError`\s.
|
|
356
285
|
|
|
357
286
|
.. versionadded:: 3.8
|
|
358
287
|
|
|
359
288
|
"""
|
|
360
289
|
|
|
361
|
-
def __init__(self, msg: str, excs: Sequence[
|
|
290
|
+
def __init__(self, msg: str, excs: Sequence[_UpdateHookErrorT], /):
|
|
362
291
|
super().__init__(msg, excs)
|
|
363
292
|
for e in self.exceptions:
|
|
364
293
|
if not isinstance(e, UpdateHookError):
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
294
|
+
raise TypeError(
|
|
295
|
+
"UpdateHookErrorGroup can only contain UpdateHookError; "
|
|
296
|
+
f"got {type(e).__name__}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# https://github.com/python/typeshed/issues/9922
|
|
300
|
+
def derive( # type: ignore[override]
|
|
301
|
+
self, excs: Sequence[_UpdateHookErrorT], /
|
|
302
|
+
) -> ExceptionGroup[_UpdateHookErrorT]:
|
|
369
303
|
return UpdateHookErrorGroup(self.message, excs)
|
|
370
304
|
|
|
371
305
|
|
reader/types.py
CHANGED
|
@@ -18,7 +18,7 @@ from typing import Literal
|
|
|
18
18
|
from typing import NamedTuple
|
|
19
19
|
from typing import overload
|
|
20
20
|
from typing import Protocol
|
|
21
|
-
from typing import
|
|
21
|
+
from typing import Self
|
|
22
22
|
from typing import TypedDict
|
|
23
23
|
from typing import Union
|
|
24
24
|
|
|
@@ -29,9 +29,6 @@ from reader.exceptions import UpdateError
|
|
|
29
29
|
from reader._utils import MISSING as MISSING # isort: skip # noqa: F401
|
|
30
30
|
from reader._utils import MissingType as MissingType # isort: skip # noqa: F401
|
|
31
31
|
|
|
32
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
33
|
-
from typing_extensions import Self
|
|
34
|
-
|
|
35
32
|
|
|
36
33
|
class _namedtuple_compat:
|
|
37
34
|
"""Add namedtuple-like methods to a dataclass."""
|
|
@@ -726,26 +723,81 @@ class EntryUpdateStatus(enum.Enum):
|
|
|
726
723
|
MODIFIED = 'modified'
|
|
727
724
|
|
|
728
725
|
|
|
729
|
-
|
|
726
|
+
class FeedSort(enum.StrEnum):
|
|
727
|
+
"""How to order feeds.
|
|
728
|
+
|
|
729
|
+
.. versionadded:: 3.18
|
|
730
|
+
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
#: By :attr:`~Feed.resolved_title`, case insensitive.
|
|
734
|
+
TITLE = 'title'
|
|
735
|
+
|
|
736
|
+
#: By :attr:`~Feed.added`, last added first.
|
|
737
|
+
ADDED = 'added'
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class EntrySort(enum.StrEnum):
|
|
741
|
+
"""How to order entries.
|
|
730
742
|
|
|
743
|
+
.. versionadded:: 3.18
|
|
744
|
+
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
#: Most recent first. That is:
|
|
748
|
+
#:
|
|
749
|
+
#: * by published date for entries imported on the first update
|
|
750
|
+
#: (if an entry does not have :attr:`~Entry.published`,
|
|
751
|
+
#: :attr:`~Entry.updated` is used)
|
|
752
|
+
#: * by added date for entries imported after that
|
|
753
|
+
#:
|
|
754
|
+
#: This is to make sure newly imported entries appear at the top
|
|
755
|
+
#: regardless of when the feed says they were published,
|
|
756
|
+
#: while not having all the old entries at the top for new feeds.
|
|
757
|
+
#:
|
|
758
|
+
#: .. note::
|
|
759
|
+
#:
|
|
760
|
+
#: The algorithm for "recent" is a heuristic and may change over time.
|
|
761
|
+
#:
|
|
762
|
+
#: .. versionchanged:: 3.1
|
|
763
|
+
#: Sort entries by added date most of the time,
|
|
764
|
+
#: with the exception of those imported on the first update.
|
|
765
|
+
#: Previously, entries would be sorted by added
|
|
766
|
+
#: only if they were published less than 7 days ago.
|
|
767
|
+
#:
|
|
768
|
+
RECENT = 'recent'
|
|
731
769
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
770
|
+
#: Random (shuffle). Return at most 256 entries.
|
|
771
|
+
#:
|
|
772
|
+
#: .. versionadded:: 1.2
|
|
773
|
+
#:
|
|
774
|
+
RANDOM = 'random'
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class EntrySearchSort(enum.StrEnum):
|
|
778
|
+
"""How to order entry search results.
|
|
779
|
+
|
|
780
|
+
.. versionadded:: 3.18
|
|
781
|
+
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
#: Most relevant first.
|
|
785
|
+
RELEVANT = 'relevant'
|
|
786
|
+
|
|
787
|
+
#: Most recent first. See :data:`EntrySort.RECENT` for details.
|
|
788
|
+
#:
|
|
789
|
+
#: .. versionadded:: 1.4
|
|
790
|
+
#:
|
|
791
|
+
RECENT = 'recent'
|
|
792
|
+
|
|
793
|
+
#: Random (shuffle). See :data:`EntrySort.RANDOM` for details.
|
|
794
|
+
#:
|
|
795
|
+
#: .. versionadded:: 1.10
|
|
796
|
+
#:
|
|
797
|
+
RANDOM = 'random'
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
# Semi-public API (typing support)
|
|
749
801
|
|
|
750
802
|
|
|
751
803
|
# https://github.com/python/typing/issues/182
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.19.dev0
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
7
7
|
Project-URL: Documentation, https://reader.readthedocs.io/
|
|
8
|
-
Project-URL:
|
|
9
|
-
Project-URL:
|
|
10
|
-
Project-URL:
|
|
8
|
+
Project-URL: Repository, https://github.com/lemon24/reader
|
|
9
|
+
Project-URL: Issues, https://github.com/lemon24/reader/issues
|
|
10
|
+
Project-URL: Changelog, https://reader.readthedocs.io/en/latest/changelog.html
|
|
11
11
|
Keywords: atom,cdf,feed,rdf,rss,json feed,web feed,podcast,feed reader,feed aggregator
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Environment :: Console
|
|
@@ -19,10 +19,10 @@ Classifier: Operating System :: OS Independent
|
|
|
19
19
|
Classifier: Programming Language :: Python
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
26
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
27
27
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
28
28
|
Classifier: Topic :: Internet
|
|
@@ -32,7 +32,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
|
32
32
|
Classifier: Topic :: Software Development :: Libraries
|
|
33
33
|
Classifier: Topic :: Utilities
|
|
34
34
|
Classifier: Typing :: Typed
|
|
35
|
-
Requires-Python: >=3.
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
36
|
Description-Content-Type: text/x-rst
|
|
37
37
|
License-File: LICENSE
|
|
38
38
|
Requires-Dist: typing-extensions>=4
|
|
@@ -41,14 +41,14 @@ Requires-Dist: requests>=2.18
|
|
|
41
41
|
Requires-Dist: werkzeug>2
|
|
42
42
|
Requires-Dist: iso8601>=1
|
|
43
43
|
Requires-Dist: beautifulsoup4>=4.5
|
|
44
|
-
Provides-Extra:
|
|
45
|
-
|
|
44
|
+
Provides-Extra: all
|
|
45
|
+
Requires-Dist: reader[app,cli,unstable-plugins]; extra == "all"
|
|
46
46
|
Provides-Extra: cli
|
|
47
47
|
Requires-Dist: click>=7; extra == "cli"
|
|
48
48
|
Requires-Dist: PyYAML; extra == "cli"
|
|
49
49
|
Provides-Extra: app
|
|
50
50
|
Requires-Dist: flask>=0.10; extra == "app"
|
|
51
|
-
Requires-Dist: humanize
|
|
51
|
+
Requires-Dist: humanize>=4; extra == "app"
|
|
52
52
|
Requires-Dist: PyYAML; extra == "app"
|
|
53
53
|
Requires-Dist: WTForms; extra == "app"
|
|
54
54
|
Requires-Dist: jinja2-fragments; extra == "app"
|
|
@@ -60,36 +60,7 @@ Requires-Dist: beautifulsoup4; extra == "unstable-plugins"
|
|
|
60
60
|
Requires-Dist: blinker>=1.4; extra == "unstable-plugins"
|
|
61
61
|
Requires-Dist: beautifulsoup4; extra == "unstable-plugins"
|
|
62
62
|
Requires-Dist: tabulate; extra == "unstable-plugins"
|
|
63
|
-
|
|
64
|
-
Requires-Dist: pytest>=4; extra == "tests"
|
|
65
|
-
Requires-Dist: pytest-randomly; extra == "tests"
|
|
66
|
-
Requires-Dist: pytest-subtests; extra == "tests"
|
|
67
|
-
Requires-Dist: pytest-rerunfailures; extra == "tests"
|
|
68
|
-
Requires-Dist: coverage; extra == "tests"
|
|
69
|
-
Requires-Dist: pytest-cov; extra == "tests"
|
|
70
|
-
Requires-Dist: requests-mock; extra == "tests"
|
|
71
|
-
Requires-Dist: mechanicalsoup; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
|
|
72
|
-
Requires-Dist: requests-wsgi-adapter; extra == "tests"
|
|
73
|
-
Requires-Dist: lxml; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
|
|
74
|
-
Requires-Dist: html5lib; extra == "tests"
|
|
75
|
-
Requires-Dist: numpy; (implementation_name != "pypy" and os_name == "posix" and python_version <= "3.12") and extra == "tests"
|
|
76
|
-
Requires-Dist: mypy; implementation_name != "pypy" and extra == "tests"
|
|
77
|
-
Requires-Dist: types-requests; extra == "tests"
|
|
78
|
-
Requires-Dist: types-beautifulsoup4; extra == "tests"
|
|
79
|
-
Provides-Extra: docs
|
|
80
|
-
Requires-Dist: sphinx; extra == "docs"
|
|
81
|
-
Requires-Dist: sphinx-rtd-theme>=1.3.0rc1; extra == "docs"
|
|
82
|
-
Requires-Dist: click>=7; extra == "docs"
|
|
83
|
-
Requires-Dist: sphinx-click; extra == "docs"
|
|
84
|
-
Requires-Dist: sphinx-hoverxref; extra == "docs"
|
|
85
|
-
Requires-Dist: sphinxcontrib-log-cabinet; extra == "docs"
|
|
86
|
-
Requires-Dist: setuptools; extra == "docs"
|
|
87
|
-
Provides-Extra: dev
|
|
88
|
-
Requires-Dist: reader[app,cli,docs,tests,unstable-plugins]; extra == "dev"
|
|
89
|
-
Requires-Dist: tox; extra == "dev"
|
|
90
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
91
|
-
Requires-Dist: build; extra == "dev"
|
|
92
|
-
Requires-Dist: twine; extra == "dev"
|
|
63
|
+
Dynamic: license-file
|
|
93
64
|
|
|
94
65
|
.. begin-intro
|
|
95
66
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
reader/__init__.py,sha256
|
|
1
|
+
reader/__init__.py,sha256=_9p_wEikp-LHY2B-Vq2TGYZj5g12NJJpP2QzJqdUYj8,2852
|
|
2
2
|
reader/__main__.py,sha256=gNiypbMW5x2B5Pn3xzMSVl7zldI824eEBg-WmzeD1vM,382
|
|
3
3
|
reader/_cli.py,sha256=gCh_D166fLeMHS4vWWagCI9H-3EJ4WxhmJ96RwS8PEY,12371
|
|
4
4
|
reader/_config.py,sha256=zQT1XMauermsQONVGuboeNjV7I03Aq8QsyuOBcaqAtM,4587
|
|
5
5
|
reader/_hash_utils.py,sha256=X28ufDQrWcoXRm4x0JCLc4Cpoyi1UKb2geiwFebpTYM,2981
|
|
6
|
-
reader/_types.py,sha256=
|
|
7
|
-
reader/_update.py,sha256=
|
|
6
|
+
reader/_types.py,sha256=1WPcoVd6mUtkwon-fJ0z6pXLc6Y4d3tAIHlBsjDReDc,41207
|
|
7
|
+
reader/_update.py,sha256=mnZCy8C1VbQ-5phK6o7JPLZid8OW069pwRTnbxd5P5Q,18257
|
|
8
8
|
reader/_utils.py,sha256=PwpqOSj1l3cG8k11z_9rO9gqVYp0CyYTorIIT856t-4,7740
|
|
9
|
-
reader/core.py,sha256=
|
|
10
|
-
reader/exceptions.py,sha256=
|
|
9
|
+
reader/core.py,sha256=PCdzZUGRlmGoJfF7AfGHV7STwEWWrYAKCX6RT_fhuQg,79818
|
|
10
|
+
reader/exceptions.py,sha256=REAWBsWrV22RfaFWxqkGAbAwz-COkp-yjHWvIYk7w1E,9961
|
|
11
11
|
reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
reader/types.py,sha256=
|
|
12
|
+
reader/types.py,sha256=Od_-4SxehrsLyjIfASrjA7AD_w_VeHtnLymmyJwCvag,34205
|
|
13
13
|
reader/utils.py,sha256=UjIjdNgI7APluejh7nMkGN91sT4JFZxWrcYOAG3J52Y,2535
|
|
14
14
|
reader/_app/__init__.py,sha256=nH1sfDP0gG5kBPaMCp-g55E0B2UZ9F-a8VQZHktVYHE,24192
|
|
15
15
|
reader/_app/api_thing.py,sha256=y88NNCGzQe8jND-2IYLhCXtD9LhshTU266iPiQbb7lU,4330
|
|
@@ -32,34 +32,34 @@ reader/_app/v2/static/theme.js,sha256=aboAfzLaiaHqpsNV6YzjxZks5chCqx-cIpQHGgMdNb
|
|
|
32
32
|
reader/_app/v2/templates/v2/entries.html,sha256=u0rbPrzLZPRTMkgSdbViHJhjiNJ_QeQlz4Z-ARDYR0U,7170
|
|
33
33
|
reader/_app/v2/templates/v2/layout.html,sha256=5f_azghWBOrxSYkBF-k0QKyh9hFz88SjAKyjqzrnfgw,4247
|
|
34
34
|
reader/_app/v2/templates/v2/macros.html,sha256=fU5HBfVCABKWuz8z6ml53yIjChxvkvX9r9EY6eaC9g0,1367
|
|
35
|
-
reader/_parser/__init__.py,sha256=
|
|
35
|
+
reader/_parser/__init__.py,sha256=j4k-nbFqgqv82zQJJYTB6c2Ev4fAvb6kwD8LT5OFzqo,13406
|
|
36
36
|
reader/_parser/_http_utils.py,sha256=jy0kJMhbQagpNnZ_cF53pynd43VijsqIN7Q35fNs84c,406
|
|
37
|
-
reader/_parser/_lazy.py,sha256=
|
|
37
|
+
reader/_parser/_lazy.py,sha256=yLLX-8bYXBX2cxzq0G8a1KHKsL1_1kWk9UX_OM_WYts,18558
|
|
38
38
|
reader/_parser/_url_utils.py,sha256=gpDQl2MDM_VCcHGjrIZFyGrjU9JrzAKrMvKfwp2iOWo,4470
|
|
39
39
|
reader/_parser/feedparser.py,sha256=2oVQGKPGNNBvRF5QlLPQXtKPiGzi_WHjDYxjhsM-1Eg,6288
|
|
40
40
|
reader/_parser/file.py,sha256=M-iqeoQkcFpAehP4sTNzoyjFGwxLZZO04Iz5ABah5lc,1501
|
|
41
41
|
reader/_parser/http.py,sha256=H1UVvGPLRFIo2fBXvw_-5DbHef8QEFMoZ__GpeTbZic,3865
|
|
42
42
|
reader/_parser/jsonfeed.py,sha256=qFLVifbkhOkW3pvMMLB2Dwz_iibah1hZlcyhnHdci8k,5642
|
|
43
43
|
reader/_parser/requests/__init__.py,sha256=zEvj3jda9_bFdNtW4KFL3AGWReFUhKXCPDs80LB556M,5107
|
|
44
|
-
reader/_parser/requests/_lazy.py,sha256=
|
|
44
|
+
reader/_parser/requests/_lazy.py,sha256=zSnNWQnlKi7ChKG7tI3pOQ16K5vc6beZOMadLDej_pw,4986
|
|
45
45
|
reader/_plugins/__init__.py,sha256=C7U9M_pTe9Qul3IVaxrm411222AElnPgAQBhad21MME,1617
|
|
46
|
-
reader/_plugins/cli_status.py,sha256=
|
|
47
|
-
reader/_plugins/enclosure_tags.py,sha256=
|
|
46
|
+
reader/_plugins/cli_status.py,sha256=cW8fwPKdYa80ve1Nv6XUKXtWW0gXMwyenIZFkSz5XX8,4195
|
|
47
|
+
reader/_plugins/enclosure_tags.py,sha256=70sQ-8YkjLKCsMCAtcJHc-jI-cYB4G6fOFUkliJvRmY,4487
|
|
48
48
|
reader/_plugins/preview_feed_list.py,sha256=u9_c0EMj0uwWKlsOXSuGM554J4rqEyskuUOG8u306MY,4562
|
|
49
49
|
reader/_plugins/share.py,sha256=02GBA9XXKkRrtfyyUk9mr8hx9jhLdbES-NrAHvs6M4I,1227
|
|
50
50
|
reader/_plugins/sqlite_releases.py,sha256=R14lNGmsk-LuyOueaScYk6Qf_sjVILlTKaMDuE-1c6o,3036
|
|
51
51
|
reader/_plugins/timer.py,sha256=1cjxVPjwTJDV6N0TA9-1RGlVNI0D70_aAAIfQy2lEqI,5962
|
|
52
52
|
reader/_plugins/templates/preview_feed_list.html,sha256=aFfcbXLuf3bHtkYlH4tgwK7ip1Ukx93WS6X6fntYDwU,815
|
|
53
53
|
reader/_storage/__init__.py,sha256=1WbFjzSqflwuSo8AWpAlDiLshkQ3zDAuasWfMKkmaow,1132
|
|
54
|
-
reader/_storage/_base.py,sha256
|
|
54
|
+
reader/_storage/_base.py,sha256=-_7vyKa6xEbYqkhaOKMw2MdgyHzsmoJHJVSkE00qbVA,3232
|
|
55
55
|
reader/_storage/_changes.py,sha256=PrOgZrkmQLbI4iOKQpDMta0xgswBeqQq0_sNfqbKZ0A,6298
|
|
56
|
-
reader/_storage/_entries.py,sha256=
|
|
57
|
-
reader/_storage/_feeds.py,sha256=
|
|
56
|
+
reader/_storage/_entries.py,sha256=YlPvupxRKHfPuUvoZy2uJT7Sl1f2Hi7UD872xAZ6MIM,23494
|
|
57
|
+
reader/_storage/_feeds.py,sha256=GFr1OGG4j86ZEtmVzfOlgXK_n1BthBkp0ExYmEkx784,11887
|
|
58
58
|
reader/_storage/_html_utils.py,sha256=qwVAfPk3VAkG6vWWna5js20vW5bc2Oe3xv-QYrDiJLE,2061
|
|
59
59
|
reader/_storage/_schema.py,sha256=sEGEhF8Atc6AKOVHHaeNYrmum0GB9SNkNpNIspT5-9o,10274
|
|
60
|
-
reader/_storage/_search.py,sha256=
|
|
61
|
-
reader/_storage/_sql_utils.py,sha256=
|
|
62
|
-
reader/_storage/_sqlite_utils.py,sha256=
|
|
60
|
+
reader/_storage/_search.py,sha256=yEUCzfH6O-zNCNX1nI-Ewzy706eoQ2Fr5eZ1feHwchE,22017
|
|
61
|
+
reader/_storage/_sql_utils.py,sha256=rJJN4XckLK6UyvVl2-yEMejuCNyYIZ0-nJNt_3QlwAU,10330
|
|
62
|
+
reader/_storage/_sqlite_utils.py,sha256=y7yP-JEeDApvdjufHOM0E8NLxPCe4srQqUXBTXQa7gQ,25797
|
|
63
63
|
reader/_storage/_tags.py,sha256=0Qll-WEuu8BeKlzcAp7SadjMK46pqDzsUOzwCa1wWD0,10736
|
|
64
64
|
reader/_vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
reader/_vendor/feedparser/__init__.py,sha256=eg2_Fs6sPwVY3Pat68RoqgfDyAkP0ydyv6wRzGM96nE,3133
|
|
@@ -102,8 +102,8 @@ reader/plugins/entry_dedupe.py,sha256=hSNB0NZ7CPx34md3073j9Hyv5E49iuDHjn-yVDkQb5
|
|
|
102
102
|
reader/plugins/mark_as_read.py,sha256=j_GG3zmQtd6icr5iSLmiu7CXqcQmODLgNR91c23lxvs,3709
|
|
103
103
|
reader/plugins/readtime.py,sha256=gWlO8MOLDlOddMhc9bgdwrthV-o13_DQA2qbKGD53qw,4699
|
|
104
104
|
reader/plugins/ua_fallback.py,sha256=bwDcgKfJmTPiBa52Vb6063zAQ8Pnh9GJlxBE0ZrWMG0,1630
|
|
105
|
-
reader-3.
|
|
106
|
-
reader-3.
|
|
107
|
-
reader-3.
|
|
108
|
-
reader-3.
|
|
109
|
-
reader-3.
|
|
105
|
+
reader-3.19.dev0.dist-info/licenses/LICENSE,sha256=ndUPB_e5gQU8fDYOwbx5utcbbokCD1Joi94v_LCDBLA,1475
|
|
106
|
+
reader-3.19.dev0.dist-info/METADATA,sha256=9ArGAYa0oQ26zOA0vAfE8XdzjSaRxgsffUoWPrFUzM8,6219
|
|
107
|
+
reader-3.19.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
108
|
+
reader-3.19.dev0.dist-info/top_level.txt,sha256=zVRv6FulXZWKf9Gycz9vjxul5OCZ3fCq2Attdwsu7Z4,7
|
|
109
|
+
reader-3.19.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|