reader 3.20__tar.gz → 3.21__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.
- {reader-3.20 → reader-3.21}/CHANGES.rst +34 -0
- {reader-3.20/src/reader.egg-info → reader-3.21}/PKG-INFO +1 -1
- {reader-3.20 → reader-3.21}/docs/guide.rst +12 -5
- {reader-3.20 → reader-3.21}/pyproject.toml +4 -2
- {reader-3.20 → reader-3.21}/run.sh +1 -1
- {reader-3.20 → reader-3.21}/src/reader/__init__.py +1 -1
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/forms.py +2 -1
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/templates/v2/entries.html +2 -0
- {reader-3.20 → reader-3.21}/src/reader/_cli.py +4 -1
- {reader-3.20 → reader-3.21}/src/reader/_parser/__init__.py +52 -13
- {reader-3.20 → reader-3.21}/src/reader/_parser/_http_utils.py +9 -1
- {reader-3.20 → reader-3.21}/src/reader/_storage/_sqlite_utils.py +24 -22
- {reader-3.20 → reader-3.21}/src/reader/_update.py +11 -10
- {reader-3.20 → reader-3.21}/src/reader/core.py +23 -7
- {reader-3.20 → reader-3.21}/src/reader/plugins/entry_dedupe.py +5 -6
- {reader-3.20 → reader-3.21/src/reader.egg-info}/PKG-INFO +1 -1
- {reader-3.20 → reader-3.21}/tests/conftest.py +12 -0
- {reader-3.20 → reader-3.21}/tests/fakeparser.py +27 -36
- {reader-3.20 → reader-3.21}/tests/reader_methods.py +45 -10
- {reader-3.20 → reader-3.21}/tests/test_app.py +6 -3
- {reader-3.20 → reader-3.21}/tests/test_changes.py +3 -0
- {reader-3.20 → reader-3.21}/tests/test_cli.py +4 -4
- {reader-3.20 → reader-3.21}/tests/test_plugins_entry_dedupe.py +21 -3
- {reader-3.20 → reader-3.21}/tests/test_plugins_mark_as_read.py +3 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_readtime.py +3 -0
- {reader-3.20 → reader-3.21}/tests/test_reader.py +36 -1298
- {reader-3.20 → reader-3.21}/tests/test_reader_counts.py +1 -1
- {reader-3.20 → reader-3.21}/tests/test_reader_filter.py +103 -109
- {reader-3.20 → reader-3.21}/tests/test_reader_hooks.py +3 -0
- reader-3.21/tests/test_reader_integration.py +207 -0
- {reader-3.20 → reader-3.21}/tests/test_reader_lifecycle.py +6 -8
- {reader-3.20 → reader-3.21}/tests/test_reader_private.py +3 -0
- {reader-3.20 → reader-3.21}/tests/test_reader_search.py +15 -23
- {reader-3.20 → reader-3.21}/tests/test_reader_sort.py +7 -0
- reader-3.21/tests/test_reader_update.py +1255 -0
- {reader-3.20 → reader-3.21}/tests/test_reader_utils.py +3 -0
- {reader-3.20 → reader-3.21}/tests/utils.py +20 -0
- {reader-3.20 → reader-3.21}/tox.ini +1 -1
- reader-3.20/tests/test_reader_integration.py +0 -209
- reader-3.20/tests/test_reader_update.py +0 -172
- {reader-3.20 → reader-3.21}/LICENSE +0 -0
- {reader-3.20 → reader-3.21}/MANIFEST.in +0 -0
- {reader-3.20 → reader-3.21}/README.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/api.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/app.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/changelog.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/cli.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/conf.py +0 -0
- {reader-3.20 → reader-3.21}/docs/config.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/contributing.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/dev-app.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/dev.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/images/redesign-01.png +0 -0
- {reader-3.20 → reader-3.21}/docs/index.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/install.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/internal.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/plugins.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/dillo.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entries-feed.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entries-v2-dark.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entries-v2-filters-light.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entries.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entry-one.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/entry-two.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/feeds.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/lynx.png +0 -0
- {reader-3.20 → reader-3.21}/docs/screenshots/search.png +0 -0
- {reader-3.20 → reader-3.21}/docs/tutorial.rst +0 -0
- {reader-3.20 → reader-3.21}/docs/why.rst +0 -0
- {reader-3.20 → reader-3.21}/examples/config.yaml +0 -0
- {reader-3.20 → reader-3.21}/examples/custom_headers.py +0 -0
- {reader-3.20 → reader-3.21}/examples/feed_slugs.py +0 -0
- {reader-3.20 → reader-3.21}/examples/parser_only.py +0 -0
- {reader-3.20 → reader-3.21}/examples/podcast.py +0 -0
- {reader-3.20 → reader-3.21}/examples/terminal.py +0 -0
- {reader-3.20 → reader-3.21}/scripts/backup.sh +0 -0
- {reader-3.20 → reader-3.21}/scripts/bench.py +0 -0
- {reader-3.20 → reader-3.21}/scripts/debug_storage_stats.py +0 -0
- {reader-3.20 → reader-3.21}/scripts/entry_dedupe_backtest.py +0 -0
- {reader-3.20 → reader-3.21}/scripts/jscontrols.html +0 -0
- {reader-3.20 → reader-3.21}/scripts/jscontrols.py +0 -0
- {reader-3.20 → reader-3.21}/scripts/lines.sh +0 -0
- {reader-3.20 → reader-3.21}/scripts/release.py +0 -0
- {reader-3.20 → reader-3.21}/setup.cfg +0 -0
- {reader-3.20 → reader-3.21}/src/reader/__main__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/api_thing.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/cli.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/static/controls.js +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/static/style.css +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/add_entry.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/entries.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/entry.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/feeds.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/macros.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/metadata.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/templates/tags.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/static/style.css +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/static/theme.js +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/templates/v2/layout.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/v2/templates/v2/macros.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_app/wsgi.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_config.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_hash_utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/_lazy.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/feedparser.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/file.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/http.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/jsonfeed.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/requests/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_parser/requests/_lazy.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/enclosure_tags.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/preview_feed_list.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/share.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/sqlite_releases.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_plugins/timer.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_base.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_changes.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_entries.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_feeds.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_html_utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_schema.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_search.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_sql_utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_storage/_tags.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_types.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/exceptions.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/plugins/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/plugins/mark_as_read.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/plugins/readtime.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/plugins/ua_fallback.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/py.typed +0 -0
- {reader-3.20 → reader-3.21}/src/reader/types.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader/utils.py +0 -0
- {reader-3.20 → reader-3.21}/src/reader.egg-info/SOURCES.txt +0 -0
- {reader-3.20 → reader-3.21}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.20 → reader-3.21}/src/reader.egg-info/requires.txt +0 -0
- {reader-3.20 → reader-3.21}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.20 → reader-3.21}/tests/data/10.json +0 -0
- {reader-3.20 → reader-3.21}/tests/data/10.json.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/custom +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.atom +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.atom.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.json +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.json.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.rss +0 -0
- {reader-3.20 → reader-3.21}/tests/data/empty.rss.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.atom +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.atom.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.json +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.json.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.rss +0 -0
- {reader-3.20 → reader-3.21}/tests/data/full.rss.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/invalid.json +0 -0
- {reader-3.20 → reader-3.21}/tests/data/invalid.json.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/relative.atom +0 -0
- {reader-3.20 → reader-3.21}/tests/data/relative.atom.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/relative.rss +0 -0
- {reader-3.20 → reader-3.21}/tests/data/relative.rss.py +0 -0
- {reader-3.20 → reader-3.21}/tests/data/sqlite_releases.html +0 -0
- {reader-3.20 → reader-3.21}/tests/data/unknown.json +0 -0
- {reader-3.20 → reader-3.21}/tests/data/unknown.json.py +0 -0
- {reader-3.20 → reader-3.21}/tests/reader_test_plugins/__init__.py +0 -0
- {reader-3.20 → reader-3.21}/tests/reader_test_plugins/good.py +0 -0
- {reader-3.20 → reader-3.21}/tests/reader_test_plugins/init_error.py +0 -0
- {reader-3.20 → reader-3.21}/tests/reader_test_plugins/missing_dependency.py +0 -0
- {reader-3.20 → reader-3.21}/tests/reader_test_plugins/missing_entry_point.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test__types.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test__utils.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_app_wsgi.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_bench.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_config.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_exceptions.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_hash_utils.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_html_utils.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_lazy_imports.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_parser.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_preview_feed_list.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_reader_deprecations.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_reader_plugins.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_search.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_sql_utils.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_sqlite_utils.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_storage.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_tags.py +0 -0
- {reader-3.20 → reader-3.21}/tests/test_types.py +0 -0
|
@@ -6,6 +6,40 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.21
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2026-01-12
|
|
13
|
+
|
|
14
|
+
* :ref:`Scheduled updates <scheduled>` improvements.
|
|
15
|
+
|
|
16
|
+
* Only update scheduled feeds by default
|
|
17
|
+
(:meth:`update_feeds(scheduled=...) <Reader.update_feeds>`
|
|
18
|
+
defaults to :const:`True`);
|
|
19
|
+
previously, all feeds would be updated.
|
|
20
|
+
(:issue:`379`)
|
|
21
|
+
|
|
22
|
+
.. warning::
|
|
23
|
+
|
|
24
|
+
**This is a minor compatibility break**;
|
|
25
|
+
the previous behavior is a bug –
|
|
26
|
+
*reader* should do the right thing
|
|
27
|
+
even if the user doesn't read the documentation.
|
|
28
|
+
|
|
29
|
+
In the CLI, make ``update --scheduled`` the default.
|
|
30
|
+
|
|
31
|
+
* Honor the Cache-Control (max-age) and Expires HTTP headers.
|
|
32
|
+
(:issue:`376`)
|
|
33
|
+
|
|
34
|
+
* Limit the :attr:`~Feed.update_after` derived from HTTP headers
|
|
35
|
+
to a month in the future.
|
|
36
|
+
(:issue:`384`)
|
|
37
|
+
|
|
38
|
+
* If the Retry-After value is a date,
|
|
39
|
+
make the update interval relative to Date.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
9
43
|
Version 3.20
|
|
10
44
|
------------
|
|
11
45
|
|
|
@@ -270,11 +270,12 @@ Scheduled updates
|
|
|
270
270
|
~~~~~~~~~~~~~~~~~
|
|
271
271
|
|
|
272
272
|
Because different feeds need to be updated at different rates,
|
|
273
|
-
*reader* also provides a mechanism for scheduling updates
|
|
273
|
+
*reader* also provides a mechanism for scheduling updates
|
|
274
|
+
that is enabled by default.
|
|
274
275
|
|
|
275
276
|
On every update, *reader* determines when a feed should be updated next
|
|
276
277
|
based on a configurable update interval;
|
|
277
|
-
:meth
|
|
278
|
+
:meth:`~Reader.update_feeds`
|
|
278
279
|
updates only the feeds that should be updated at or before the current time.
|
|
279
280
|
|
|
280
281
|
The interval can be **configured by the user**
|
|
@@ -289,9 +290,9 @@ such that updates don't happen exactly at the the same time;
|
|
|
289
290
|
for example, with a one-hour interval, a jitter of 0.25 means
|
|
290
291
|
updates will occur any time in the first 15 minutes of each hour.
|
|
291
292
|
|
|
292
|
-
If the server
|
|
293
|
-
|
|
294
|
-
:meth:`update_feeds(scheduled=True) <Reader.update_feeds>` will honor
|
|
293
|
+
If the server responds with any of the
|
|
294
|
+
Cache-Control max-age, Expires, or Retry-After HTTP headers,
|
|
295
|
+
:meth:`update_feeds(scheduled=True) <Reader.update_feeds>` will honor them.
|
|
295
296
|
|
|
296
297
|
|
|
297
298
|
.. note::
|
|
@@ -308,6 +309,12 @@ If the server sends the Retry-After HTTP header with
|
|
|
308
309
|
.. versionchanged:: 3.15
|
|
309
310
|
Honor the Retry-After HTTP header.
|
|
310
311
|
|
|
312
|
+
.. versionchanged:: 3.21
|
|
313
|
+
Honor the Cache-Control max-age and Expires HTTP headers.
|
|
314
|
+
|
|
315
|
+
.. versionchanged:: 3.21
|
|
316
|
+
Only update scheduled feeds by default.
|
|
317
|
+
|
|
311
318
|
|
|
312
319
|
Update status
|
|
313
320
|
~~~~~~~~~~~~~
|
|
@@ -92,9 +92,8 @@ unstable-plugins = [
|
|
|
92
92
|
[dependency-groups]
|
|
93
93
|
|
|
94
94
|
tests = [
|
|
95
|
-
"pytest>=
|
|
95
|
+
"pytest>=9",
|
|
96
96
|
"pytest-randomly",
|
|
97
|
-
"pytest-subtests",
|
|
98
97
|
"pytest-rerunfailures",
|
|
99
98
|
"coverage",
|
|
100
99
|
"requests-mock",
|
|
@@ -118,6 +117,8 @@ typing = [
|
|
|
118
117
|
|
|
119
118
|
docs = [
|
|
120
119
|
"sphinx",
|
|
120
|
+
# TODO: remove when https://github.com/sphinx-doc/sphinx/issues/14186 is solved
|
|
121
|
+
"roman_numerals<4.1",
|
|
121
122
|
"sphinx-rtd-theme",
|
|
122
123
|
"click>=7",
|
|
123
124
|
"sphinx-click",
|
|
@@ -149,6 +150,7 @@ markers = [
|
|
|
149
150
|
"slow: mark a test as slow.",
|
|
150
151
|
"requires_lxml: mark a test to only run in places where we have lxml.",
|
|
151
152
|
"apptest: mark a test as a web app test (skipped sometimes).",
|
|
153
|
+
"noscheduled: force update_feeds(scheduled=...) to false.",
|
|
152
154
|
"noautoclose: skip 'autoclose' autouse fixture for a test."
|
|
153
155
|
]
|
|
154
156
|
filterwarnings = [
|
|
@@ -116,7 +116,8 @@ ENTRY_SORT_CHOICES = ['recent', 'random']
|
|
|
116
116
|
class EntryFilter(PresetsMixin, Form):
|
|
117
117
|
feed = HiddenField("feed")
|
|
118
118
|
search = SearchField("search", name='Q')
|
|
119
|
-
feed_tags = TagFilterField("tags", name='tags')
|
|
119
|
+
feed_tags = TagFilterField("feed tags", name='tags')
|
|
120
|
+
tags = TagFilterField("entry tags", name='entry-tags')
|
|
120
121
|
read = radio_field("read", choices=BOOL_CHOICES, default='no')
|
|
121
122
|
important = radio_field("important", choices=TRISTATE_CHOICES, default='maybe')
|
|
122
123
|
has_enclosures = radio_field(
|
|
@@ -52,9 +52,11 @@
|
|
|
52
52
|
{# TODO: if data or error #}
|
|
53
53
|
{% if form.search.data %}{{ macros.input(form.search, 'search') }}{% endif %}
|
|
54
54
|
{% if form.feed_tags.data %}{{ macros.input(form.feed_tags, 'tag') }}{% endif %}
|
|
55
|
+
{% if form.feed_tags.data %}{{ macros.input(form.tags, 'tag') }}{% endif %}
|
|
55
56
|
<div class="collapse mb-3" id="nav-more">
|
|
56
57
|
{% if not form.search.data %}{{ macros.input(form.search, 'search') }}{% endif %}
|
|
57
58
|
{% if not form.feed_tags.data %}{{ macros.input(form.feed_tags, 'tag') }}{% endif %}
|
|
59
|
+
{% if not form.feed_tags.data %}{{ macros.input(form.tags, 'tag') }}{% endif %}
|
|
58
60
|
<div class="mb-3">
|
|
59
61
|
{{ macros.radio(form.read) }}
|
|
60
62
|
{{ macros.radio(form.important) }}
|
|
@@ -290,7 +290,10 @@ def iter_update_status(it, length):
|
|
|
290
290
|
help="Only update new (never updated before) feeds.",
|
|
291
291
|
)
|
|
292
292
|
@click.option(
|
|
293
|
-
'--scheduled/--no-scheduled',
|
|
293
|
+
'--scheduled/--no-scheduled',
|
|
294
|
+
default=True,
|
|
295
|
+
show_default=True,
|
|
296
|
+
help="Only update feeds scheduled to be updated.",
|
|
294
297
|
)
|
|
295
298
|
@click.option(
|
|
296
299
|
'--workers',
|
|
@@ -8,6 +8,7 @@ from contextlib import contextmanager
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from datetime import timedelta
|
|
11
|
+
from datetime import timezone
|
|
11
12
|
from typing import Any
|
|
12
13
|
from typing import cast
|
|
13
14
|
from typing import ContextManager
|
|
@@ -34,6 +35,8 @@ from .requests import TimeoutType
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING: # pragma: no cover
|
|
38
|
+
from werkzeug.datastructures import RequestCacheControl
|
|
39
|
+
|
|
37
40
|
from ._lazy import Parser as Parser
|
|
38
41
|
|
|
39
42
|
|
|
@@ -189,28 +192,64 @@ class HTTPInfo(_namedtuple_compat):
|
|
|
189
192
|
#: The HTTP response headers.
|
|
190
193
|
headers: Headers
|
|
191
194
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
def get_update_after(self, now: datetime) -> datetime | None:
|
|
196
|
+
"""Select the best "update after" date from available headers."""
|
|
197
|
+
rv = []
|
|
198
|
+
|
|
199
|
+
if self.status in (429, 503):
|
|
200
|
+
try:
|
|
201
|
+
seconds = int(self.headers.get('retry-after', ''))
|
|
202
|
+
rv.append(now + timedelta(seconds=seconds))
|
|
203
|
+
except ValueError:
|
|
204
|
+
if retry_after := self.parse_date('retry-after', now):
|
|
205
|
+
rv.append(retry_after)
|
|
206
|
+
|
|
207
|
+
# https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
|
|
208
|
+
if cache_control := self.cache_control:
|
|
209
|
+
if not cache_control.no_cache:
|
|
210
|
+
if max_age := cache_control.max_age:
|
|
211
|
+
rv.append(now + timedelta(seconds=max_age))
|
|
212
|
+
elif expires := self.parse_date('expires', now):
|
|
213
|
+
rv.append(expires)
|
|
214
|
+
|
|
215
|
+
# TODO: RFC 9111 specifies a Last-Modified fallback heuristic,
|
|
216
|
+
# but it might be better to implement it in the updater
|
|
217
|
+
# as part of https://github.com/lemon24/reader/issues/382
|
|
218
|
+
|
|
219
|
+
return max(rv, default=None)
|
|
220
|
+
|
|
221
|
+
def parse_date(self, name: str, now: datetime | None = None) -> datetime | None:
|
|
222
|
+
"""Parse an HTTP date header and return a timezone-aware datetime.
|
|
223
|
+
|
|
224
|
+
Return None if missing or if parsing fails.
|
|
195
225
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if timezone information is missing, UTC is assumed.
|
|
226
|
+
If `now` is given and the Date header is set,
|
|
227
|
+
make the returned value relative to `now`.
|
|
199
228
|
|
|
200
229
|
"""
|
|
201
230
|
# lazy import
|
|
202
231
|
from ._http_utils import parse_date
|
|
203
232
|
|
|
204
|
-
value
|
|
233
|
+
if value := parse_date(self.headers.get(name, '')):
|
|
234
|
+
value = value.astimezone(timezone.utc)
|
|
235
|
+
if now and (date := parse_date(self.headers.get('date', ''))):
|
|
236
|
+
value = now + (value - date)
|
|
237
|
+
return value
|
|
238
|
+
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def cache_control(self) -> RequestCacheControl | None:
|
|
243
|
+
"""Parsed Cache-Control header, or None if missing."""
|
|
244
|
+
|
|
245
|
+
# lazy import
|
|
246
|
+
from ._http_utils import parse_cache_control_header
|
|
247
|
+
|
|
248
|
+
value = self.headers.get('cache-control')
|
|
205
249
|
if not value:
|
|
206
250
|
return None
|
|
207
251
|
|
|
208
|
-
|
|
209
|
-
seconds = int(value)
|
|
210
|
-
except ValueError:
|
|
211
|
-
return parse_date(value)
|
|
212
|
-
|
|
213
|
-
return timedelta(seconds=seconds)
|
|
252
|
+
return parse_cache_control_header(value)
|
|
214
253
|
|
|
215
254
|
|
|
216
255
|
class RetrieveError(ParseError):
|
|
@@ -4,8 +4,11 @@ HTTP utilities. Contains no business logic.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from collections.abc import Iterable
|
|
7
|
+
from functools import partial
|
|
7
8
|
|
|
8
9
|
import werkzeug.http
|
|
10
|
+
from werkzeug.datastructures import MIMEAccept
|
|
11
|
+
from werkzeug.datastructures import ResponseCacheControl
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
parse_options_header = werkzeug.http.parse_options_header
|
|
@@ -14,4 +17,9 @@ parse_date = werkzeug.http.parse_date
|
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def unparse_accept_header(values: Iterable[tuple[str, float]]) -> str:
|
|
17
|
-
return
|
|
20
|
+
return MIMEAccept(values).to_header()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
parse_cache_control_header = partial(
|
|
24
|
+
werkzeug.http.parse_cache_control_header, cls=ResponseCacheControl
|
|
25
|
+
)
|
|
@@ -20,6 +20,7 @@ from contextlib import closing
|
|
|
20
20
|
from contextlib import contextmanager
|
|
21
21
|
from contextlib import nullcontext
|
|
22
22
|
from dataclasses import dataclass
|
|
23
|
+
from dataclasses import field
|
|
23
24
|
from datetime import datetime
|
|
24
25
|
from datetime import timezone
|
|
25
26
|
from pathlib import Path
|
|
@@ -440,6 +441,19 @@ class LocalConnectionFactory:
|
|
|
440
441
|
|
|
441
442
|
INLINE_OPTIMIZE_TIMEOUT = 0.1
|
|
442
443
|
|
|
444
|
+
@dataclass
|
|
445
|
+
class _State(threading.local):
|
|
446
|
+
class _Sentinel:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
db: sqlite3.Connection | None = None
|
|
450
|
+
finalizer: weakref.finalize[[sqlite3.Connection], _Sentinel] | None = None
|
|
451
|
+
finalizer_sentinel: _Sentinel = field(default_factory=_Sentinel)
|
|
452
|
+
is_creating_thread: bool = False
|
|
453
|
+
context_stack: list[None] = field(default_factory=list)
|
|
454
|
+
call_count: int = 0
|
|
455
|
+
closed: bool = False
|
|
456
|
+
|
|
443
457
|
def __init__(
|
|
444
458
|
self,
|
|
445
459
|
path: str,
|
|
@@ -455,15 +469,14 @@ class LocalConnectionFactory:
|
|
|
455
469
|
raise NotImplementedError("is_private() does not work for uri=True")
|
|
456
470
|
self.kwargs['uri'] = True
|
|
457
471
|
self.attached: dict[str, str] = {}
|
|
458
|
-
self._local =
|
|
472
|
+
self._local = self._State()
|
|
459
473
|
self._local.is_creating_thread = True
|
|
460
474
|
self._other_threads = False
|
|
461
475
|
# connect immediately, so exceptions are raised before the first call
|
|
462
476
|
self.__call__()
|
|
463
477
|
|
|
464
478
|
def __call__(self) -> sqlite3.Connection:
|
|
465
|
-
db
|
|
466
|
-
if db:
|
|
479
|
+
if db := self._local.db:
|
|
467
480
|
if not self._local.context_stack:
|
|
468
481
|
if self._should_optimize(self._local.call_count):
|
|
469
482
|
self._optimize(db, self.read_only, self.INLINE_OPTIMIZE_TIMEOUT)
|
|
@@ -482,15 +495,11 @@ class LocalConnectionFactory:
|
|
|
482
495
|
if not self._local.is_creating_thread:
|
|
483
496
|
self._other_threads = True
|
|
484
497
|
|
|
485
|
-
|
|
486
|
-
db = cast(
|
|
498
|
+
self._local.db = db = cast(
|
|
487
499
|
sqlite3.Connection,
|
|
488
500
|
sqlite3.connect(self._make_uri(self.path), **self.kwargs),
|
|
489
501
|
)
|
|
490
502
|
|
|
491
|
-
self._local.db = db
|
|
492
|
-
self._local.call_count = 0
|
|
493
|
-
|
|
494
503
|
try:
|
|
495
504
|
self.setup_db(db)
|
|
496
505
|
except BaseException:
|
|
@@ -501,8 +510,13 @@ class LocalConnectionFactory:
|
|
|
501
510
|
# works on cpython (finalizer runs in thread),
|
|
502
511
|
# but not on pypy (finalizer runs in main thread);
|
|
503
512
|
# also see https://bugs.python.org/issue14073
|
|
513
|
+
#
|
|
514
|
+
# originally, we were using threading.current_thread() as target,
|
|
515
|
+
# but an unrelated object owned by the thread is more reliable
|
|
516
|
+
# (the thread is collected with a delay, and rarely only atexit)
|
|
517
|
+
#
|
|
504
518
|
self._local.finalizer = weakref.finalize(
|
|
505
|
-
|
|
519
|
+
self._local.finalizer_sentinel, self._close, db, self.read_only
|
|
506
520
|
) # type: ignore[call-arg]
|
|
507
521
|
|
|
508
522
|
for name, path in self.attached.items():
|
|
@@ -541,7 +555,7 @@ class LocalConnectionFactory:
|
|
|
541
555
|
return
|
|
542
556
|
# can't close() a connection from a thread that didn't create it;
|
|
543
557
|
# SQLAlchemy ignores this as well in SingletonThreadPool.dispose()
|
|
544
|
-
if "objects created in a thread" in message:
|
|
558
|
+
if "objects created in a thread" in message: # pragma: no cover
|
|
545
559
|
return
|
|
546
560
|
raise
|
|
547
561
|
|
|
@@ -643,18 +657,6 @@ class LocalConnectionFactory:
|
|
|
643
657
|
return path in TEMPORARY_DB_PATHS
|
|
644
658
|
|
|
645
659
|
|
|
646
|
-
class _LocalConnectionFactoryState(threading.local):
|
|
647
|
-
def __init__(self) -> None:
|
|
648
|
-
self.db: sqlite3.Connection | None = None
|
|
649
|
-
self.finalizer: (
|
|
650
|
-
weakref.finalize[[sqlite3.Connection], threading.Thread] | None
|
|
651
|
-
) = None
|
|
652
|
-
self.is_creating_thread: bool = False
|
|
653
|
-
self.context_stack: list[None] = []
|
|
654
|
-
self.call_count: int = 0
|
|
655
|
-
self.closed: bool = False
|
|
656
|
-
|
|
657
|
-
|
|
658
660
|
@contextmanager
|
|
659
661
|
def busy_timeout(
|
|
660
662
|
db: sqlite3.Connection, seconds: float
|
|
@@ -5,6 +5,7 @@ import random
|
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
|
+
from datetime import timedelta
|
|
8
9
|
from datetime import timezone
|
|
9
10
|
from functools import partial
|
|
10
11
|
from itertools import chain
|
|
@@ -252,17 +253,16 @@ class Decider:
|
|
|
252
253
|
value = self.get_feed_to_update(result.value, bool(entries_to_update))
|
|
253
254
|
|
|
254
255
|
update_after = next_update_after(self.global_now, **self.config)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
else:
|
|
261
|
-
retry_after = self.global_now + http_info.retry_after
|
|
262
|
-
# also accounts for retry_after being in the past / negative
|
|
263
|
-
if retry_after > update_after:
|
|
256
|
+
if result.http_info:
|
|
257
|
+
# TODO (#376): technically this is supposed to be against request end
|
|
258
|
+
http_update_after = result.http_info.get_update_after(self.global_now)
|
|
259
|
+
# also accounts for it being in the past / negative
|
|
260
|
+
if http_update_after and http_update_after > update_after:
|
|
264
261
|
# round up to the next interval
|
|
265
|
-
|
|
262
|
+
# TODO: don't round up if already on the next interval
|
|
263
|
+
update_after = next_update_after(http_update_after, **self.config)
|
|
264
|
+
# limit excessive update_after
|
|
265
|
+
update_after = min(update_after, self.global_now + MAX_UPDATE_AFTER)
|
|
266
266
|
|
|
267
267
|
# We always return a FeedUpdateIntent because
|
|
268
268
|
# we always want to set last_retrieved and update_after,
|
|
@@ -324,6 +324,7 @@ def set_number(name, src, dst, type, min=0, max=float('inf')): # type: ignore
|
|
|
324
324
|
# start on a Monday, so weekly amounts of seconds line up
|
|
325
325
|
UPDATE_AFTER_START = datetime(1970, 1, 5)
|
|
326
326
|
EPOCH_OFFSET = (UPDATE_AFTER_START - datetime(1970, 1, 1)).total_seconds()
|
|
327
|
+
MAX_UPDATE_AFTER = timedelta(31)
|
|
327
328
|
|
|
328
329
|
|
|
329
330
|
def next_update_after(now: datetime, interval: int, jitter: float = 0) -> datetime:
|
|
@@ -388,6 +388,9 @@ class Reader:
|
|
|
388
388
|
self._enable_search = _enable_search
|
|
389
389
|
self._update_hooks = UpdateHooks(self)
|
|
390
390
|
|
|
391
|
+
#: Override update_feeds(scheduled=...).
|
|
392
|
+
self._scheduled_override = None
|
|
393
|
+
|
|
391
394
|
if _called_directly:
|
|
392
395
|
warnings.warn(
|
|
393
396
|
"Reader objects should be created using make_reader(); the Reader "
|
|
@@ -795,7 +798,7 @@ class Reader:
|
|
|
795
798
|
broken: bool | None = None,
|
|
796
799
|
updates_enabled: bool | None = True,
|
|
797
800
|
new: bool | None = None,
|
|
798
|
-
scheduled: bool =
|
|
801
|
+
scheduled: bool = True,
|
|
799
802
|
workers: int = 1,
|
|
800
803
|
) -> None:
|
|
801
804
|
r"""Update all or some of the feeds.
|
|
@@ -808,7 +811,7 @@ class Reader:
|
|
|
808
811
|
currently, only the exceptions for the first 5 feeds
|
|
809
812
|
with hook failures are collected.
|
|
810
813
|
|
|
811
|
-
By default, update all
|
|
814
|
+
By default, update all scheduled feeds that have updates enabled.
|
|
812
815
|
|
|
813
816
|
Roughly equivalent to ``for _ in reader.update_feeds_iter(...): pass``.
|
|
814
817
|
|
|
@@ -826,6 +829,7 @@ class Reader:
|
|
|
826
829
|
/ have been updated before. Defaults to None.
|
|
827
830
|
scheduled (bool):
|
|
828
831
|
Only update feeds scheduled to be updated.
|
|
832
|
+
Defaults to true.
|
|
829
833
|
workers (int): Number of threads to use when getting the feeds.
|
|
830
834
|
|
|
831
835
|
Raises:
|
|
@@ -876,6 +880,9 @@ class Reader:
|
|
|
876
880
|
``new`` uses :attr:`~Feed.last_retrieved`
|
|
877
881
|
instead of :attr:`~Feed.last_updated`.
|
|
878
882
|
|
|
883
|
+
.. versionchanged:: 3.21
|
|
884
|
+
Only update scheduled feeds by default.
|
|
885
|
+
|
|
879
886
|
"""
|
|
880
887
|
with self._update_hooks.group("some hooks failed") as hook_errors:
|
|
881
888
|
results = self.update_feeds_iter(
|
|
@@ -913,7 +920,7 @@ class Reader:
|
|
|
913
920
|
broken: bool | None = None,
|
|
914
921
|
updates_enabled: bool | None = True,
|
|
915
922
|
new: bool | None = None,
|
|
916
|
-
scheduled: bool =
|
|
923
|
+
scheduled: bool = True,
|
|
917
924
|
workers: int = 1,
|
|
918
925
|
_call_feeds_update_hooks: bool = True,
|
|
919
926
|
) -> Iterable[UpdateResult]:
|
|
@@ -927,7 +934,7 @@ class Reader:
|
|
|
927
934
|
and re-raise them as an :exc:`UpdateHookErrorGroup`
|
|
928
935
|
after updating all the feeds.
|
|
929
936
|
|
|
930
|
-
By default, update all
|
|
937
|
+
By default, update all scheduled feeds that have updates enabled.
|
|
931
938
|
|
|
932
939
|
Args:
|
|
933
940
|
feed (str or tuple(str) or Feed or None): Only update the feed with this URL.
|
|
@@ -943,6 +950,7 @@ class Reader:
|
|
|
943
950
|
/ have been updated before. Defaults to None.
|
|
944
951
|
scheduled (bool):
|
|
945
952
|
Only update feeds scheduled to be updated.
|
|
953
|
+
Defaults to true.
|
|
946
954
|
workers (int): Number of threads to use when getting the feeds.
|
|
947
955
|
|
|
948
956
|
Yields:
|
|
@@ -1006,8 +1014,15 @@ class Reader:
|
|
|
1006
1014
|
``new`` uses :attr:`~Feed.last_retrieved`
|
|
1007
1015
|
instead of :attr:`~Feed.last_updated`.
|
|
1008
1016
|
|
|
1017
|
+
.. versionchanged:: 3.21
|
|
1018
|
+
Only update scheduled feeds by default.
|
|
1019
|
+
|
|
1009
1020
|
"""
|
|
1010
1021
|
now = self._now()
|
|
1022
|
+
|
|
1023
|
+
if self._scheduled_override is not None: # pragma: no cover
|
|
1024
|
+
scheduled = self._scheduled_override
|
|
1025
|
+
|
|
1011
1026
|
filter = FeedFilter.from_args(
|
|
1012
1027
|
now, feed, tags, broken, updates_enabled, new, scheduled
|
|
1013
1028
|
)
|
|
@@ -1076,7 +1091,10 @@ class Reader:
|
|
|
1076
1091
|
"""
|
|
1077
1092
|
_, rv = zero_or_one(
|
|
1078
1093
|
self.update_feeds_iter(
|
|
1079
|
-
feed=feed,
|
|
1094
|
+
feed=feed,
|
|
1095
|
+
updates_enabled=None,
|
|
1096
|
+
scheduled=False,
|
|
1097
|
+
_call_feeds_update_hooks=False,
|
|
1080
1098
|
),
|
|
1081
1099
|
lambda: FeedNotFoundError(_feed_argument(feed)),
|
|
1082
1100
|
)
|
|
@@ -1624,8 +1642,6 @@ class Reader:
|
|
|
1624
1642
|
recent_sort = self._storage.get_entry_recent_sort(src_entry.resource_id)
|
|
1625
1643
|
dst_resource_id = _entry_argument(dst)
|
|
1626
1644
|
|
|
1627
|
-
# FIXME: do not allow copy to the same feed (or at least entry)
|
|
1628
|
-
|
|
1629
1645
|
attrs = dict(src_entry.__dict__)
|
|
1630
1646
|
attrs['feed_url'], attrs['id'] = dst_resource_id
|
|
1631
1647
|
|
|
@@ -7,13 +7,12 @@ reader.entry_dedupe
|
|
|
7
7
|
|
|
8
8
|
Deduplicate the entries of a feed.
|
|
9
9
|
|
|
10
|
-
Sometimes, the
|
|
10
|
+
Sometimes, the id of some or all the entries in a feed changes
|
|
11
11
|
(e.g. from ``example.com/123`` to ``example.com/entry-title``),
|
|
12
12
|
causing each entry to appear twice.
|
|
13
|
-
:mod:`~reader.plugins.entry_dedupe`
|
|
14
|
-
by copying
|
|
15
|
-
|
|
16
|
-
and **deleting** the old entries.
|
|
13
|
+
:mod:`~reader.plugins.entry_dedupe` fixes this
|
|
14
|
+
by copying user attributes to the new entry
|
|
15
|
+
and deleting the old one.
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
User attributes
|
|
@@ -605,7 +604,7 @@ def normalize_url(url):
|
|
|
605
604
|
|
|
606
605
|
try:
|
|
607
606
|
url = urlparse(url)
|
|
608
|
-
except ValueError:
|
|
607
|
+
except ValueError:
|
|
609
608
|
return None
|
|
610
609
|
|
|
611
610
|
scheme = url.scheme.lower()
|
|
@@ -11,6 +11,7 @@ import reader_methods
|
|
|
11
11
|
from fakeparser import Parser
|
|
12
12
|
from reader import make_reader as original_make_reader
|
|
13
13
|
from reader._storage import Storage
|
|
14
|
+
from utils import monkeypatch_datetime
|
|
14
15
|
from utils import monkeypatch_os
|
|
15
16
|
from utils import monkeypatch_tz
|
|
16
17
|
from utils import reload_module
|
|
@@ -74,6 +75,9 @@ def make_reader(request):
|
|
|
74
75
|
if 'parser' in request.fixturenames:
|
|
75
76
|
reader._parser = request.getfixturevalue('parser')
|
|
76
77
|
|
|
78
|
+
if 'noscheduled' in request.keywords:
|
|
79
|
+
reader._scheduled_override = False
|
|
80
|
+
|
|
77
81
|
return reader
|
|
78
82
|
|
|
79
83
|
return make_reader
|
|
@@ -86,6 +90,9 @@ def reader(request):
|
|
|
86
90
|
if 'parser' in request.fixturenames:
|
|
87
91
|
reader._parser = request.getfixturevalue('parser')
|
|
88
92
|
|
|
93
|
+
if 'noscheduled' in request.keywords:
|
|
94
|
+
reader._scheduled_override = False
|
|
95
|
+
|
|
89
96
|
yield reader
|
|
90
97
|
|
|
91
98
|
|
|
@@ -124,6 +131,11 @@ def update_feeds_iter(request):
|
|
|
124
131
|
return request.param
|
|
125
132
|
|
|
126
133
|
|
|
134
|
+
@pytest.fixture(params=reader_methods.update_feeds_methods)
|
|
135
|
+
def update_feeds(request):
|
|
136
|
+
return request.param
|
|
137
|
+
|
|
138
|
+
|
|
127
139
|
def feed_arg_as_str(feed):
|
|
128
140
|
return feed.url
|
|
129
141
|
|