reader 3.22__tar.gz → 3.24__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.22 → reader-3.24}/CHANGES.rst +62 -0
- {reader-3.22/src/reader.egg-info → reader-3.24}/PKG-INFO +4 -3
- {reader-3.22 → reader-3.24}/README.rst +1 -0
- {reader-3.22 → reader-3.24}/docs/api.rst +29 -0
- {reader-3.22 → reader-3.24}/docs/app.rst +23 -0
- {reader-3.22 → reader-3.24}/docs/conf.py +3 -2
- {reader-3.22 → reader-3.24}/docs/dev.rst +0 -1
- {reader-3.22 → reader-3.24}/docs/guide.rst +83 -25
- {reader-3.22 → reader-3.24}/docs/install.rst +2 -1
- reader-3.24/docs/screenshots/empty.png +0 -0
- reader-3.24/docs/screenshots/feeds.png +0 -0
- reader-3.24/docs/screenshots/help.png +0 -0
- reader-3.24/docs/screenshots/import-result.png +0 -0
- reader-3.24/docs/screenshots/import-select.png +0 -0
- {reader-3.22 → reader-3.24}/examples/podcast.py +0 -1
- {reader-3.22 → reader-3.24}/pyproject.toml +7 -4
- {reader-3.22 → reader-3.24}/scripts/bench.py +0 -1
- {reader-3.22 → reader-3.24}/scripts/debug_storage_stats.py +0 -1
- {reader-3.22 → reader-3.24}/scripts/entry_dedupe_backtest.py +0 -1
- {reader-3.22 → reader-3.24}/scripts/jscontrols.py +0 -2
- {reader-3.22 → reader-3.24}/scripts/vendor_app_stuff.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/__init__.py +10 -3
- {reader-3.22 → reader-3.24}/src/reader/__main__.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/__init__.py +50 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/ext.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/__init__.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/entry.html +9 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/wsgi.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/static/style.css +6 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/add_feed.html +10 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/change_feed_title.html +1 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/delete_feed.html +1 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/entries.html +16 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/entry.html +1 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/feeds.html +13 -2
- reader-3.24/src/reader/_app/templates/help.html +131 -0
- reader-3.24/src/reader/_app/templates/import_feeds.html +131 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/layout_base.html +14 -1
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/macros.html +62 -9
- {reader-3.22 → reader-3.24}/src/reader/_app/wsgi.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_cli.py +17 -7
- {reader-3.22 → reader-3.24}/src/reader/_config_utils.py +7 -3
- {reader-3.22 → reader-3.24}/src/reader/_hash_utils.py +0 -1
- reader-3.24/src/reader/_logging.py +80 -0
- {reader-3.22 → reader-3.24}/src/reader/_parser/__init__.py +17 -10
- {reader-3.22 → reader-3.24}/src/reader/_parser/_http_utils.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_parser/_lazy.py +50 -72
- {reader-3.22 → reader-3.24}/src/reader/_parser/feedparser.py +93 -13
- {reader-3.22 → reader-3.24}/src/reader/_parser/jsonfeed.py +29 -32
- {reader-3.22 → reader-3.24}/src/reader/_parser/requests/__init__.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_parser/requests/_lazy.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_plugins/legacy/enclosure_tags.py +1 -2
- {reader-3.22 → reader-3.24}/src/reader/_plugins/legacy/preview_feed_list.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_plugins/legacy/share.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_plugins/sqlite_releases.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/__init__.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/_base.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/_changes.py +2 -5
- {reader-3.22 → reader-3.24}/src/reader/_storage/_entries.py +62 -31
- {reader-3.22 → reader-3.24}/src/reader/_storage/_feeds.py +66 -41
- {reader-3.22 → reader-3.24}/src/reader/_storage/_html_utils.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/_schema.py +49 -10
- {reader-3.22 → reader-3.24}/src/reader/_storage/_search.py +16 -35
- {reader-3.22 → reader-3.24}/src/reader/_storage/_sql_utils.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/_sqlite_utils.py +2 -1
- {reader-3.22 → reader-3.24}/src/reader/_storage/_tags.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/_types.py +15 -100
- reader-3.22/src/reader/_update.py → reader-3.24/src/reader/_update/__init__.py +95 -197
- reader-3.24/src/reader/_update/base.py +205 -0
- reader-3.24/src/reader/_update/hooks.py +105 -0
- {reader-3.22 → reader-3.24}/src/reader/_utils.py +25 -62
- {reader-3.22 → reader-3.24}/src/reader/core.py +86 -37
- {reader-3.22 → reader-3.24}/src/reader/exceptions.py +8 -0
- reader-3.24/src/reader/opml.py +178 -0
- {reader-3.22 → reader-3.24}/src/reader/plugins/__init__.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/plugins/_loader.py +0 -1
- {reader-3.22 → reader-3.24}/src/reader/plugins/entry_dedupe.py +31 -45
- {reader-3.22 → reader-3.24}/src/reader/plugins/mark_as_read.py +5 -8
- {reader-3.22 → reader-3.24}/src/reader/plugins/readtime.py +8 -11
- {reader-3.22 → reader-3.24}/src/reader/plugins/ua_fallback.py +3 -11
- {reader-3.22 → reader-3.24}/src/reader/types.py +134 -10
- {reader-3.22 → reader-3.24}/src/reader/utils.py +16 -2
- {reader-3.22 → reader-3.24/src/reader.egg-info}/PKG-INFO +4 -3
- {reader-3.22 → reader-3.24}/src/reader.egg-info/SOURCES.txt +14 -1
- {reader-3.22 → reader-3.24}/src/reader.egg-info/requires.txt +2 -2
- {reader-3.22 → reader-3.24}/tests/conftest.py +3 -39
- {reader-3.22 → reader-3.24}/tests/data/10.json.py +0 -1
- {reader-3.22 → reader-3.24}/tests/data/empty.atom.py +1 -1
- {reader-3.22 → reader-3.24}/tests/data/empty.json.py +0 -1
- {reader-3.22 → reader-3.24}/tests/data/empty.rss.py +0 -1
- {reader-3.22 → reader-3.24}/tests/data/full.atom.py +4 -4
- {reader-3.22 → reader-3.24}/tests/data/full.json.py +7 -4
- {reader-3.22 → reader-3.24}/tests/data/full.rss.py +3 -3
- {reader-3.22 → reader-3.24}/tests/data/invalid.json.py +3 -3
- {reader-3.22 → reader-3.24}/tests/data/relative.atom.py +0 -1
- {reader-3.22 → reader-3.24}/tests/data/relative.rss.py +0 -1
- {reader-3.22 → reader-3.24}/tests/data/unknown.json.py +0 -1
- reader-3.24/tests/fixtures.py +48 -0
- {reader-3.22 → reader-3.24}/tests/test__types.py +14 -6
- {reader-3.22 → reader-3.24}/tests/test__utils.py +0 -20
- {reader-3.22 → reader-3.24}/tests/test_app_legacy.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_bench.py +0 -2
- {reader-3.22 → reader-3.24}/tests/test_changes.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_cli.py +18 -4
- {reader-3.22 → reader-3.24}/tests/test_config_utils.py +10 -4
- {reader-3.22 → reader-3.24}/tests/test_html_utils.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_lazy_imports.py +2 -7
- reader-3.24/tests/test_logging.py +41 -0
- reader-3.24/tests/test_opml.py +153 -0
- {reader-3.22 → reader-3.24}/tests/test_parser.py +201 -13
- {reader-3.22 → reader-3.24}/tests/test_plugins_entry_dedupe.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_plugins_mark_as_read.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_plugins_preview_feed_list.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_plugins_readtime.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_plugins_ua_fallback.py +1 -1
- {reader-3.22 → reader-3.24}/tests/test_reader.py +16 -13
- {reader-3.22 → reader-3.24}/tests/test_reader_deprecations.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_reader_filter.py +25 -0
- {reader-3.22 → reader-3.24}/tests/test_reader_hooks.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_reader_lifecycle.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_reader_private.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_reader_search.py +9 -9
- {reader-3.22 → reader-3.24}/tests/test_reader_sort.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_reader_update.py +11 -10
- {reader-3.22 → reader-3.24}/tests/test_reader_utils.py +14 -2
- {reader-3.22 → reader-3.24}/tests/test_search.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_sql_utils.py +12 -22
- {reader-3.22 → reader-3.24}/tests/test_sqlite_utils.py +0 -1
- {reader-3.22 → reader-3.24}/tests/test_storage.py +28 -0
- {reader-3.22 → reader-3.24}/tests/test_types.py +33 -0
- reader-3.22/docs/screenshots/feeds.png +0 -0
- {reader-3.22 → reader-3.24}/LICENSE +0 -0
- {reader-3.22 → reader-3.24}/MANIFEST.in +0 -0
- {reader-3.22 → reader-3.24}/docs/changelog.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/cli.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/config.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/contributing.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/index.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/internal.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/plugins.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/screenshots/entries-filters-light.png +0 -0
- {reader-3.22 → reader-3.24}/docs/screenshots/entries.png +0 -0
- {reader-3.22 → reader-3.24}/docs/screenshots/entry-image-light.png +0 -0
- {reader-3.22 → reader-3.24}/docs/screenshots/entry.png +0 -0
- {reader-3.22 → reader-3.24}/docs/screenshots/feed.png +0 -0
- {reader-3.22 → reader-3.24}/docs/tutorial.rst +0 -0
- {reader-3.22 → reader-3.24}/docs/why.rst +0 -0
- {reader-3.22 → reader-3.24}/examples/config.toml +0 -0
- {reader-3.22 → reader-3.24}/examples/custom_headers.py +0 -0
- {reader-3.22 → reader-3.24}/examples/feed_slugs.py +0 -0
- {reader-3.22 → reader-3.24}/examples/parser_only.py +0 -0
- {reader-3.22 → reader-3.24}/examples/terminal.py +0 -0
- {reader-3.22 → reader-3.24}/run.sh +0 -0
- {reader-3.22 → reader-3.24}/scripts/backup.sh +0 -0
- {reader-3.22 → reader-3.24}/scripts/jscontrols.html +0 -0
- {reader-3.22 → reader-3.24}/scripts/lines.sh +0 -0
- {reader-3.22 → reader-3.24}/scripts/release.py +0 -0
- {reader-3.22 → reader-3.24}/setup.cfg +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/cli.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/forms.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/api_thing.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/static/controls.js +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/static/style.css +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/add_entry.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/entries.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/feeds.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/layout.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/macros.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/metadata.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/legacy/templates/tags.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/theme.js +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/README +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_parser/file.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_parser/http.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_plugins/legacy/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_plugins/timer.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.22 → reader-3.24}/src/reader/py.typed +0 -0
- {reader-3.22 → reader-3.24}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.22 → reader-3.24}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.22 → reader-3.24}/tests/_plugins/__init__.py +0 -0
- {reader-3.22 → reader-3.24}/tests/_plugins/builtin.py +0 -0
- {reader-3.22 → reader-3.24}/tests/_plugins/error_during_import.py +0 -0
- {reader-3.22 → reader-3.24}/tests/_plugins/error_import_error.py +0 -0
- {reader-3.22 → reader-3.24}/tests/data/10.json +0 -0
- {reader-3.22 → reader-3.24}/tests/data/custom +0 -0
- {reader-3.22 → reader-3.24}/tests/data/empty.atom +0 -0
- {reader-3.22 → reader-3.24}/tests/data/empty.json +0 -0
- {reader-3.22 → reader-3.24}/tests/data/empty.rss +0 -0
- {reader-3.22 → reader-3.24}/tests/data/full.atom +0 -0
- {reader-3.22 → reader-3.24}/tests/data/full.json +0 -0
- {reader-3.22 → reader-3.24}/tests/data/full.rss +0 -0
- {reader-3.22 → reader-3.24}/tests/data/invalid.json +0 -0
- {reader-3.22 → reader-3.24}/tests/data/relative.atom +0 -0
- {reader-3.22 → reader-3.24}/tests/data/relative.rss +0 -0
- {reader-3.22 → reader-3.24}/tests/data/sqlite_releases.html +0 -0
- {reader-3.22 → reader-3.24}/tests/data/unknown.json +0 -0
- {reader-3.22 → reader-3.24}/tests/fakeparser.py +0 -0
- {reader-3.22 → reader-3.24}/tests/reader_methods.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_app_wsgi.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_exceptions.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_hash_utils.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_plugins_loader.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_reader_counts.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_reader_integration.py +0 -0
- {reader-3.22 → reader-3.24}/tests/test_tags.py +0 -0
- {reader-3.22 → reader-3.24}/tests/utils.py +0 -0
- {reader-3.22 → reader-3.24}/tox.ini +0 -0
|
@@ -6,6 +6,68 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.24
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2026-05-20
|
|
13
|
+
|
|
14
|
+
* Support multiple authors with details (link, email).
|
|
15
|
+
Thanks to `Anshul Mittal`_ for the PR.
|
|
16
|
+
(:issue:`391`)
|
|
17
|
+
|
|
18
|
+
* Add :attr:`Feed.authors`, :attr:`Entry.authors`,
|
|
19
|
+
and :attr:`EntrySource.authors` attributes.
|
|
20
|
+
* Add matching :attr:`~Feed.authors_str` property.
|
|
21
|
+
* Deprecate :attr:`~Feed.author` attribute.
|
|
22
|
+
|
|
23
|
+
* Fix bug introduced in `version 3.15 <Version 3.15_>`__
|
|
24
|
+
causing feeds to not be retrieved in parallel
|
|
25
|
+
even if :meth:`~Reader.update_feeds` was called with ``workers`` > 1.
|
|
26
|
+
|
|
27
|
+
* Use :ref:`structured logging <logging>` internally,
|
|
28
|
+
with the possibility to opt into native `structlog`_ logging.
|
|
29
|
+
(:issue:`147`)
|
|
30
|
+
|
|
31
|
+
* Use :meth:`datetime.datetime.fromisoformat` instead of external dependency ``iso8601``.
|
|
32
|
+
Thanks to `Sebastian Bradley`_ for the PR.
|
|
33
|
+
(:issue:`398`)
|
|
34
|
+
|
|
35
|
+
* Refactor updater internals to allow reusing logic in alternate implementations.
|
|
36
|
+
|
|
37
|
+
* :doc:`app` improvements.
|
|
38
|
+
|
|
39
|
+
* Add empty state and contextual help; :ref:`screenshots <screenshots-help>`.
|
|
40
|
+
* Import feeds improvements (select all/none, accept only XML / OPML files).
|
|
41
|
+
|
|
42
|
+
* Add ``--json`` option to the ``list`` CLI commands.
|
|
43
|
+
Thanks to `Puneet Dixit`_ for the PR.
|
|
44
|
+
(:issue:`394`)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
.. _Puneet Dixit: https://github.com/puneetdixit200
|
|
49
|
+
.. _Anshul Mittal: https://github.com/anderson688
|
|
50
|
+
.. _Sebastian Bradley: https://github.com/bazbrad765
|
|
51
|
+
|
|
52
|
+
.. _structlog: https://www.structlog.org/
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Version 3.23
|
|
56
|
+
------------
|
|
57
|
+
|
|
58
|
+
Released 2026-04-24
|
|
59
|
+
|
|
60
|
+
* Add support for :ref:`opml`. (:issue:`165`)
|
|
61
|
+
|
|
62
|
+
* Add support for import / export in the web app; :ref:`screenshots <screenshots-import>`.
|
|
63
|
+
|
|
64
|
+
* Deprecate the :func:`~reader.utils.archive_entries` ``feed_user_title`` argument.
|
|
65
|
+
* Fix :meth:`get_feeds(feed='') <Reader.get_feeds>` and
|
|
66
|
+
:meth:`get_entries(entry=('', '')) <Reader.get_entries>`
|
|
67
|
+
returning wrong entries.
|
|
68
|
+
(:issue:`392`)
|
|
69
|
+
|
|
70
|
+
|
|
9
71
|
Version 3.22
|
|
10
72
|
------------
|
|
11
73
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.24
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -39,12 +39,12 @@ Requires-Dist: typing-extensions>=4
|
|
|
39
39
|
Requires-Dist: feedparser>=6
|
|
40
40
|
Requires-Dist: requests>=2.18
|
|
41
41
|
Requires-Dist: werkzeug>2
|
|
42
|
-
Requires-Dist: iso8601>=1
|
|
43
42
|
Requires-Dist: beautifulsoup4>=4.5
|
|
43
|
+
Requires-Dist: structlog
|
|
44
44
|
Provides-Extra: all
|
|
45
45
|
Requires-Dist: reader[app,cli,unstable-plugins]; extra == "all"
|
|
46
46
|
Provides-Extra: cli
|
|
47
|
-
Requires-Dist: click
|
|
47
|
+
Requires-Dist: click!=8.4.0,>=7; extra == "cli"
|
|
48
48
|
Provides-Extra: app
|
|
49
49
|
Requires-Dist: flask>=0.10; extra == "app"
|
|
50
50
|
Requires-Dist: humanize>=4; extra == "app"
|
|
@@ -113,6 +113,7 @@ you'd probably call it a fat model.
|
|
|
113
113
|
* filter feeds and articles
|
|
114
114
|
* full-text search articles
|
|
115
115
|
* get statistics on feed and user activity
|
|
116
|
+
* import / export feeds as OPML
|
|
116
117
|
* write plugins to extend its functionality
|
|
117
118
|
* skip all the low level stuff and focus on what makes your feed reader different
|
|
118
119
|
|
|
@@ -49,6 +49,7 @@ you'd probably call it a fat model.
|
|
|
49
49
|
* filter feeds and articles
|
|
50
50
|
* full-text search articles
|
|
51
51
|
* get statistics on feed and user activity
|
|
52
|
+
* import / export feeds as OPML
|
|
52
53
|
* write plugins to extend its functionality
|
|
53
54
|
* skip all the low level stuff and focus on what makes your feed reader different
|
|
54
55
|
|
|
@@ -27,12 +27,17 @@ Data objects
|
|
|
27
27
|
|
|
28
28
|
.. autoclass:: Feed
|
|
29
29
|
:members:
|
|
30
|
+
:inherited-members:
|
|
31
|
+
|
|
32
|
+
.. autoclass:: Author
|
|
33
|
+
:members:
|
|
30
34
|
|
|
31
35
|
.. autoclass:: ExceptionInfo
|
|
32
36
|
:members:
|
|
33
37
|
|
|
34
38
|
.. autoclass:: Entry
|
|
35
39
|
:members:
|
|
40
|
+
:inherited-members:
|
|
36
41
|
|
|
37
42
|
.. autoclass:: Content
|
|
38
43
|
:members:
|
|
@@ -42,6 +47,7 @@ Data objects
|
|
|
42
47
|
|
|
43
48
|
.. autoclass:: EntrySource
|
|
44
49
|
:members:
|
|
50
|
+
:inherited-members:
|
|
45
51
|
|
|
46
52
|
.. autoclass:: EntrySearchResult
|
|
47
53
|
:members:
|
|
@@ -67,6 +73,17 @@ Data objects
|
|
|
67
73
|
.. autoclass:: EntryUpdateStatus
|
|
68
74
|
:members:
|
|
69
75
|
|
|
76
|
+
.. autoclass:: FeedToImport
|
|
77
|
+
:members:
|
|
78
|
+
:undoc-members:
|
|
79
|
+
|
|
80
|
+
.. autoclass:: FeedImportResult
|
|
81
|
+
:members:
|
|
82
|
+
|
|
83
|
+
.. autoclass:: FeedExport
|
|
84
|
+
:members:
|
|
85
|
+
|
|
86
|
+
|
|
70
87
|
|
|
71
88
|
Exceptions
|
|
72
89
|
----------
|
|
@@ -155,6 +172,9 @@ Exceptions
|
|
|
155
172
|
.. autoexception:: PluginInitError
|
|
156
173
|
:show-inheritance:
|
|
157
174
|
|
|
175
|
+
.. autoexception:: FeedImportError
|
|
176
|
+
:show-inheritance:
|
|
177
|
+
|
|
158
178
|
|
|
159
179
|
.. autoexception:: ReaderWarning
|
|
160
180
|
:show-inheritance:
|
|
@@ -213,4 +233,13 @@ Constants
|
|
|
213
233
|
Utilities
|
|
214
234
|
---------
|
|
215
235
|
|
|
236
|
+
.. autofunction:: reader.enable_structlog
|
|
237
|
+
|
|
216
238
|
.. autofunction:: reader.utils.archive_entries
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
``reader.opml``
|
|
243
|
+
---------------
|
|
244
|
+
|
|
245
|
+
.. automodule:: reader.opml
|
|
@@ -62,6 +62,29 @@ Screenshots
|
|
|
62
62
|
|
|
63
63
|
article view (light mode)
|
|
64
64
|
|
|
65
|
+
.. _screenshots-import:
|
|
66
|
+
|
|
67
|
+
.. figure:: screenshots/import-select.png
|
|
68
|
+
:width: 240px
|
|
69
|
+
|
|
70
|
+
import feeds (select)
|
|
71
|
+
|
|
72
|
+
.. figure:: screenshots/import-result.png
|
|
73
|
+
:width: 240px
|
|
74
|
+
|
|
75
|
+
import feeds (result)
|
|
76
|
+
|
|
77
|
+
.. _screenshots-help:
|
|
78
|
+
|
|
79
|
+
.. figure:: screenshots/empty.png
|
|
80
|
+
:width: 240px
|
|
81
|
+
|
|
82
|
+
new user / empty state
|
|
83
|
+
|
|
84
|
+
.. figure:: screenshots/help.png
|
|
85
|
+
:width: 240px
|
|
86
|
+
|
|
87
|
+
contextual help
|
|
65
88
|
|
|
66
89
|
|
|
67
90
|
Serving the web app
|
|
@@ -6,7 +6,6 @@ import unittest.mock
|
|
|
6
6
|
import packaging.version
|
|
7
7
|
from setuptools.config.pyprojecttoml import read_configuration
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
sys.path.insert(0, os.path.abspath('../src'))
|
|
11
10
|
|
|
12
11
|
# mock some things "by hand", so we can import reader below without any dependencies
|
|
@@ -20,12 +19,14 @@ for name in [
|
|
|
20
19
|
'werkzeug.exceptions',
|
|
21
20
|
'werkzeug.http',
|
|
22
21
|
'yaml',
|
|
22
|
+
'structlog',
|
|
23
|
+
'structlog.contextvars',
|
|
24
|
+
'structlog.stdlib',
|
|
23
25
|
]:
|
|
24
26
|
sys.modules[name] = unittest.mock.Mock()
|
|
25
27
|
|
|
26
28
|
import reader
|
|
27
29
|
|
|
28
|
-
|
|
29
30
|
extensions = [
|
|
30
31
|
'sphinx_rtd_theme',
|
|
31
32
|
'sphinx.ext.autodoc',
|
|
@@ -32,7 +32,6 @@ in my spare time,
|
|
|
32
32
|
but I will prioritize supporting :doc:`contributors <contributing>`
|
|
33
33
|
(discussions, reviews and so on).
|
|
34
34
|
|
|
35
|
-
* OPML support, :issue:`165`
|
|
36
35
|
* :ref:`deleting entries <deleting entries>`
|
|
37
36
|
|
|
38
37
|
* archiving important entries of deleted feeds, :issue:`290`
|
|
@@ -374,7 +374,7 @@ As seen in the previous sections,
|
|
|
374
374
|
updated=datetime.datetime(2020, 2, 28, 9, 34, 2, tzinfo=datetime.timezone.utc),
|
|
375
375
|
title='Hello Internet',
|
|
376
376
|
link='http://www.hellointernet.fm/',
|
|
377
|
-
|
|
377
|
+
authors=(Author(name='CGP Grey'),),
|
|
378
378
|
subtitle='CGP Grey and Brady Haran talk about YouTube, life, work, whatever.',
|
|
379
379
|
version='rss20',
|
|
380
380
|
user_title=None,
|
|
@@ -386,9 +386,10 @@ As seen in the previous sections,
|
|
|
386
386
|
To get all the feeds, use the :meth:`~Reader.get_feeds` method::
|
|
387
387
|
|
|
388
388
|
>>> for feed in reader.get_feeds():
|
|
389
|
+
... authors = ", ".join(a.name for a in feed.authors if a.name) or 'unknown author'
|
|
389
390
|
... print(
|
|
390
391
|
... feed.title or feed.url,
|
|
391
|
-
... f"by {
|
|
392
|
+
... f"by {authors},",
|
|
392
393
|
... f"updated on {feed.updated or 'never'}",
|
|
393
394
|
... )
|
|
394
395
|
...
|
|
@@ -705,35 +706,32 @@ you can delete entries using the low-level
|
|
|
705
706
|
|
|
706
707
|
|
|
707
708
|
|
|
708
|
-
..
|
|
709
|
+
.. _opml:
|
|
709
710
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
:meth:`~Reader.get_feeds`, :meth:`~Reader.get_entries`,
|
|
714
|
-
and :meth:`~Reader.search_entries`
|
|
715
|
-
can be used in a paginated fashion.
|
|
716
|
-
|
|
717
|
-
The ``limit`` argument allows limiting the number of results returned;
|
|
718
|
-
the ``starting_after`` argument allows skipping results until after
|
|
719
|
-
a specific one.
|
|
711
|
+
OPML import / export
|
|
712
|
+
--------------------
|
|
720
713
|
|
|
721
|
-
|
|
714
|
+
You can import and export feeds from and to an `OPML subscription list`_
|
|
715
|
+
by using the :meth:`~Reader.import_feeds`
|
|
716
|
+
and :meth:`~Reader.export_feeds` methods::
|
|
722
717
|
|
|
723
|
-
>>>
|
|
724
|
-
|
|
718
|
+
>>> export = reader.export_feeds()
|
|
719
|
+
>>> export.filename
|
|
720
|
+
'reader-feeds-2026-04-24-10-00-00.opml'
|
|
721
|
+
>>> with open(export.filename, 'wb') as f:
|
|
722
|
+
... f.write(export.content)
|
|
725
723
|
...
|
|
726
|
-
|
|
727
|
-
|
|
724
|
+
>>> # with another reader instance
|
|
725
|
+
>>> reader = make_reader(':memory:')
|
|
726
|
+
>>> with open(export.filename, 'rb') as f:
|
|
727
|
+
... reader.import_feeds(f)
|
|
728
728
|
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
For more detailed control over which feeds are imported,
|
|
730
|
+
see :meth:`~Reader.import_feeds_iter` and the :mod:`reader.opml` module.
|
|
731
731
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
# H.I. 134: Boxing Day
|
|
736
|
-
Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
|
|
732
|
+
.. _OPML subscription list: https://opml.org/spec2.opml#subscriptionLists
|
|
733
|
+
|
|
734
|
+
.. versionadded:: 3.23
|
|
737
735
|
|
|
738
736
|
|
|
739
737
|
|
|
@@ -831,6 +829,38 @@ Some examples of how this is useful:
|
|
|
831
829
|
|
|
832
830
|
|
|
833
831
|
|
|
832
|
+
.. _pagination:
|
|
833
|
+
|
|
834
|
+
Pagination
|
|
835
|
+
----------
|
|
836
|
+
|
|
837
|
+
:meth:`~Reader.get_feeds`, :meth:`~Reader.get_entries`,
|
|
838
|
+
and :meth:`~Reader.search_entries`
|
|
839
|
+
can be used in a paginated fashion.
|
|
840
|
+
|
|
841
|
+
The ``limit`` argument allows limiting the number of results returned;
|
|
842
|
+
the ``starting_after`` argument allows skipping results until after
|
|
843
|
+
a specific one.
|
|
844
|
+
|
|
845
|
+
To get the first page, use only ``limit``::
|
|
846
|
+
|
|
847
|
+
>>> for entry in reader.get_entries(limit=2):
|
|
848
|
+
... print(entry.title)
|
|
849
|
+
...
|
|
850
|
+
H.I. #136: Dog Bingo
|
|
851
|
+
H.I. #135: Place Your Bets
|
|
852
|
+
|
|
853
|
+
To get the next page, use the last result from a call as
|
|
854
|
+
``starting_after`` in the next call::
|
|
855
|
+
|
|
856
|
+
>>> for entry in reader.get_entries(limit=2, starting_after=entry):
|
|
857
|
+
... print(entry.title)
|
|
858
|
+
...
|
|
859
|
+
# H.I. 134: Boxing Day
|
|
860
|
+
Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
834
864
|
.. _reserved names:
|
|
835
865
|
|
|
836
866
|
Reserved names
|
|
@@ -972,6 +1002,34 @@ depending on their type attribute or feedparser defaults:
|
|
|
972
1002
|
|
|
973
1003
|
|
|
974
1004
|
|
|
1005
|
+
.. _logging:
|
|
1006
|
+
|
|
1007
|
+
Logging
|
|
1008
|
+
-------
|
|
1009
|
+
|
|
1010
|
+
*reader* uses structured logging internally, through `structlog`_.
|
|
1011
|
+
|
|
1012
|
+
By default, output goes to the standard library :mod:`logging` module;
|
|
1013
|
+
if you don't care about structlog,
|
|
1014
|
+
configure :mod:`logging` as you normally would.
|
|
1015
|
+
The *reader* root logger is called ``reader``;
|
|
1016
|
+
the names of its children are an implementation detail,
|
|
1017
|
+
and may change without any notice.
|
|
1018
|
+
|
|
1019
|
+
To enable native structlog logging,
|
|
1020
|
+
call :func:`enable_structlog` **before** configuring structlog::
|
|
1021
|
+
|
|
1022
|
+
import reader, structlog
|
|
1023
|
+
|
|
1024
|
+
reader.enable_structlog()
|
|
1025
|
+
structlog.configure(...)
|
|
1026
|
+
|
|
1027
|
+
... # do stuff with reader
|
|
1028
|
+
|
|
1029
|
+
.. _structlog: https://www.structlog.org/
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
|
|
975
1033
|
.. _errors:
|
|
976
1034
|
|
|
977
1035
|
Errors and exceptions
|
|
@@ -18,9 +18,9 @@ These packages will be installed automatically when installing *reader*:
|
|
|
18
18
|
it replaces feedparser's default use of :mod:`urllib`
|
|
19
19
|
to make it easier to write plugins.
|
|
20
20
|
* `werkzeug`_ provides HTTP utilities.
|
|
21
|
-
* `iso8601`_ parses dates in ISO 8601 / RFC 3339; used for JSON Feed parsing.
|
|
22
21
|
* `beautifulsoup4`_ is used to strip HTML tags before adding entries
|
|
23
22
|
to the search index.
|
|
23
|
+
* `structlog`_ is used for structured logging.
|
|
24
24
|
* `typing-extensions`_ is used for :mod:`typing` backports.
|
|
25
25
|
|
|
26
26
|
*reader* also depends on the :mod:`sqlite3` standard library module
|
|
@@ -64,6 +64,7 @@ As of version |version|, *reader* has the following extras:
|
|
|
64
64
|
.. _requests: https://requests.readthedocs.io/
|
|
65
65
|
.. _werkzeug: https://werkzeug.palletsprojects.com/
|
|
66
66
|
.. _iso8601: http://pyiso8601.readthedocs.org/
|
|
67
|
+
.. _structlog: https://www.structlog.org/
|
|
67
68
|
.. _typing-extensions: https://pypi.org/project/typing-extensions/
|
|
68
69
|
.. _JSON1: https://www.sqlite.org/json1.html
|
|
69
70
|
.. _FTS5: https://www.sqlite.org/fts5.html
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -40,10 +40,9 @@ dependencies = [
|
|
|
40
40
|
"requests>=2.18",
|
|
41
41
|
# for _http_utils
|
|
42
42
|
"werkzeug>2",
|
|
43
|
-
# for JSON Feed date parsing
|
|
44
|
-
"iso8601>=1",
|
|
45
43
|
# search
|
|
46
44
|
"beautifulsoup4>=4.5",
|
|
45
|
+
"structlog",
|
|
47
46
|
]
|
|
48
47
|
dynamic = ["version"]
|
|
49
48
|
|
|
@@ -60,7 +59,8 @@ Changelog = "https://reader.readthedocs.io/en/latest/changelog.html"
|
|
|
60
59
|
all = ["reader[cli,app,unstable-plugins]"]
|
|
61
60
|
|
|
62
61
|
cli = [
|
|
63
|
-
|
|
62
|
+
# != 8.4.0 because of https://github.com/pallets/click/issues/3458
|
|
63
|
+
"click>=7,!=8.4.0",
|
|
64
64
|
]
|
|
65
65
|
app = [
|
|
66
66
|
"flask>=0.10",
|
|
@@ -190,6 +190,10 @@ module = [
|
|
|
190
190
|
]
|
|
191
191
|
ignore_errors = true
|
|
192
192
|
|
|
193
|
+
[tool.black]
|
|
194
|
+
skip-string-normalization = true
|
|
195
|
+
extend-exclude = "src/reader/_vendor/.*"
|
|
196
|
+
|
|
193
197
|
[tool.isort]
|
|
194
198
|
profile = "black"
|
|
195
199
|
py_version = 311
|
|
@@ -199,4 +203,3 @@ skip_gitignore = true
|
|
|
199
203
|
filter_files = true
|
|
200
204
|
force_single_line = true
|
|
201
205
|
order_by_type = false
|
|
202
|
-
lines_after_imports = 2
|
|
@@ -8,14 +8,12 @@ from flask import jsonify
|
|
|
8
8
|
from flask import redirect
|
|
9
9
|
from flask import request
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
root_dir = os.path.dirname(__file__)
|
|
13
12
|
sys.path.insert(0, os.path.join(root_dir, '../src'))
|
|
14
13
|
|
|
15
14
|
from reader._app.api_thing import APIError
|
|
16
15
|
from reader._app.api_thing import APIThing
|
|
17
16
|
|
|
18
|
-
|
|
19
17
|
app = Flask(
|
|
20
18
|
__name__,
|
|
21
19
|
template_folder='../src/reader/_app/templates',
|
|
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
|
|
|
37
37
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
__version__ = '3.
|
|
40
|
+
__version__ = '3.24'
|
|
41
41
|
|
|
42
42
|
# isort: off
|
|
43
43
|
|
|
@@ -64,6 +64,10 @@ from .types import (
|
|
|
64
64
|
UpdateResult as UpdateResult,
|
|
65
65
|
UpdatedFeed as UpdatedFeed,
|
|
66
66
|
EntryUpdateStatus as EntryUpdateStatus,
|
|
67
|
+
FeedToImport as FeedToImport,
|
|
68
|
+
FeedImportResult as FeedImportResult,
|
|
69
|
+
FeedExport as FeedExport,
|
|
70
|
+
Author as Author,
|
|
67
71
|
)
|
|
68
72
|
|
|
69
73
|
from .exceptions import (
|
|
@@ -91,17 +95,20 @@ from .exceptions import (
|
|
|
91
95
|
InvalidPluginError as InvalidPluginError,
|
|
92
96
|
PluginInitError as PluginInitError,
|
|
93
97
|
ReaderWarning as ReaderWarning,
|
|
98
|
+
FeedImportError as FeedImportError,
|
|
94
99
|
)
|
|
95
100
|
|
|
96
|
-
|
|
97
101
|
# Constants.
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
SOURCE_URL = 'https://github.com/lemon24/reader'
|
|
104
|
+
USER_AGENT = f'python-reader/{__version__} (+{SOURCE_URL})'
|
|
100
105
|
|
|
101
106
|
|
|
102
107
|
# Prevent any logging output by default. If no handler is set,
|
|
103
108
|
# the messages bubble up to the root logger and get printed on stderr.
|
|
104
109
|
# https://docs.python.org/3/howto/logging.html#library-config
|
|
110
|
+
|
|
105
111
|
import logging # noqa: E402
|
|
112
|
+
from ._logging import enable_structlog as enable_structlog # noqa: E402
|
|
106
113
|
|
|
107
114
|
logging.getLogger('reader').addHandler(logging.NullHandler())
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import pathlib
|
|
3
|
+
from dataclasses import asdict
|
|
2
4
|
from datetime import datetime
|
|
3
5
|
from datetime import timedelta
|
|
4
6
|
from datetime import timezone
|
|
@@ -28,7 +30,9 @@ from werkzeug.exceptions import NotFound
|
|
|
28
30
|
from reader import EntryNotFoundError
|
|
29
31
|
from reader import FeedExistsError
|
|
30
32
|
from reader import FeedNotFoundError
|
|
33
|
+
from reader import FeedToImport
|
|
31
34
|
from reader import InvalidFeedURLError
|
|
35
|
+
from reader import opml
|
|
32
36
|
from reader import UpdateError
|
|
33
37
|
|
|
34
38
|
from .ext import get_reader
|
|
@@ -38,7 +42,6 @@ from .forms import ChangeFeedTitle
|
|
|
38
42
|
from .forms import EntryFilter
|
|
39
43
|
from .forms import FeedFilter
|
|
40
44
|
|
|
41
|
-
|
|
42
45
|
# for a prototype with tags and search support, see
|
|
43
46
|
# https://github.com/lemon24/reader/tree/3.21/src/reader/_app/v2
|
|
44
47
|
|
|
@@ -164,6 +167,10 @@ def feeds():
|
|
|
164
167
|
if form.validate():
|
|
165
168
|
feeds = reader.get_feeds(**kwargs)
|
|
166
169
|
|
|
170
|
+
if request.args.get('format', '').lower() == 'opml':
|
|
171
|
+
export = reader.export_feeds(feeds)
|
|
172
|
+
return export.content, export.headers
|
|
173
|
+
|
|
167
174
|
return stream_template(
|
|
168
175
|
'feeds.html',
|
|
169
176
|
form=form,
|
|
@@ -270,6 +277,38 @@ def add_feed():
|
|
|
270
277
|
return render_template('add_feed.html', form=form)
|
|
271
278
|
|
|
272
279
|
|
|
280
|
+
@blueprint.route('/feeds/import', methods=['GET', 'POST'])
|
|
281
|
+
def import_feeds():
|
|
282
|
+
reader = get_reader()
|
|
283
|
+
parsed_feeds = None
|
|
284
|
+
error = None
|
|
285
|
+
imported_feeds = None
|
|
286
|
+
|
|
287
|
+
if request.method == 'POST':
|
|
288
|
+
if file := request.files.get('file'):
|
|
289
|
+
try:
|
|
290
|
+
parsed_feeds = [asdict(f) for f in opml.parse(file.stream)]
|
|
291
|
+
except opml.OPMLError as e:
|
|
292
|
+
error = str(e)
|
|
293
|
+
else:
|
|
294
|
+
if not parsed_feeds:
|
|
295
|
+
parsed_feeds = None
|
|
296
|
+
error = "file contains no feeds"
|
|
297
|
+
else:
|
|
298
|
+
feeds = (
|
|
299
|
+
FeedToImport(**json.loads(f)) for f in request.form.getlist('feed')
|
|
300
|
+
)
|
|
301
|
+
imported_feeds = list(reader.import_feeds_iter(feeds))
|
|
302
|
+
# TODO: out of band update (unlike add, there's too many to do here)
|
|
303
|
+
|
|
304
|
+
return render_template(
|
|
305
|
+
'import_feeds.html',
|
|
306
|
+
parsed_feeds=parsed_feeds,
|
|
307
|
+
error=error,
|
|
308
|
+
imported_feeds=imported_feeds,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
273
312
|
@blueprint.route('/entry')
|
|
274
313
|
def entry():
|
|
275
314
|
reader = get_reader()
|
|
@@ -277,6 +316,11 @@ def entry():
|
|
|
277
316
|
return render_template('entry.html', entry=entry)
|
|
278
317
|
|
|
279
318
|
|
|
319
|
+
@blueprint.route('/help')
|
|
320
|
+
def help():
|
|
321
|
+
return render_template('help.html')
|
|
322
|
+
|
|
323
|
+
|
|
280
324
|
def stream_template(template_name_or_list, **kwargs):
|
|
281
325
|
# Ensure flashed messages get removed from the session,
|
|
282
326
|
# otherwise they keep adding up and never disappear.
|
|
@@ -350,6 +394,11 @@ def _find_static(folder, filename):
|
|
|
350
394
|
return matches[0]
|
|
351
395
|
|
|
352
396
|
|
|
397
|
+
@blueprint.app_template_global()
|
|
398
|
+
def is_new_user():
|
|
399
|
+
return not list(get_reader().get_feeds(limit=1))
|
|
400
|
+
|
|
401
|
+
|
|
353
402
|
def create_app(reader_config):
|
|
354
403
|
app = Flask(__name__)
|
|
355
404
|
app.config['SECRET_KEY'] = 'secret'
|