reader 3.25__tar.gz → 3.27__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.25 → reader-3.27}/CHANGES.rst +38 -2
- {reader-3.25/src/reader.egg-info → reader-3.27}/PKG-INFO +5 -3
- {reader-3.25 → reader-3.27}/docs/api.rst +9 -10
- {reader-3.25 → reader-3.27}/docs/app.rst +16 -0
- {reader-3.25 → reader-3.27}/docs/conf.py +15 -1
- {reader-3.25 → reader-3.27}/docs/guide.rst +20 -7
- {reader-3.25 → reader-3.27}/docs/install.rst +6 -8
- {reader-3.25 → reader-3.27}/docs/internal.rst +1 -1
- reader-3.27/docs/screenshots/autodiscovery.png +0 -0
- reader-3.27/docs/screenshots/export-database.png +0 -0
- {reader-3.25 → reader-3.27}/pyproject.toml +9 -4
- {reader-3.25 → reader-3.27}/scripts/release.py +1 -11
- {reader-3.25 → reader-3.27}/src/reader/__init__.py +1 -1
- {reader-3.25 → reader-3.27}/src/reader/_app/__init__.py +37 -1
- {reader-3.25 → reader-3.27}/src/reader/_app/cli.py +12 -1
- reader-3.27/src/reader/_app/exports.py +58 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/ext.py +29 -2
- {reader-3.25 → reader-3.27}/src/reader/_app/static/style.css +2 -4
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/entries.html +1 -1
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/layout_base.html +17 -1
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/macros.html +5 -4
- reader-3.27/src/reader/_app/templates/settings.html +1 -0
- reader-3.27/src/reader/_app/templates/settings_base.html +67 -0
- {reader-3.25 → reader-3.27}/src/reader/_cli.py +20 -1
- {reader-3.25 → reader-3.27}/src/reader/_parser/__init__.py +57 -140
- {reader-3.25 → reader-3.27}/src/reader/_parser/_url_utils.py +18 -0
- {reader-3.25 → reader-3.27}/src/reader/_parser/feedparser.py +3 -8
- {reader-3.25 → reader-3.27}/src/reader/_parser/file.py +2 -2
- {reader-3.25 → reader-3.27}/src/reader/_parser/http.py +69 -17
- {reader-3.25 → reader-3.27}/src/reader/_parser/jsonfeed.py +5 -14
- {reader-3.25 → reader-3.27}/src/reader/_storage/__init__.py +6 -2
- {reader-3.25 → reader-3.27}/src/reader/_storage/_base.py +52 -11
- {reader-3.25 → reader-3.27}/src/reader/_storage/_changes.py +1 -1
- {reader-3.25 → reader-3.27}/src/reader/_storage/_entries.py +8 -4
- {reader-3.25 → reader-3.27}/src/reader/_storage/_feeds.py +1 -1
- {reader-3.25 → reader-3.27}/src/reader/_storage/_html_utils.py +1 -1
- {reader-3.25 → reader-3.27}/src/reader/_storage/_search.py +1 -5
- {reader-3.25 → reader-3.27}/src/reader/_storage/_sql_utils.py +10 -16
- {reader-3.25 → reader-3.27}/src/reader/_storage/_sqlite_utils.py +3 -8
- {reader-3.25 → reader-3.27}/src/reader/_storage/_tags.py +7 -11
- {reader-3.25 → reader-3.27}/src/reader/_types.py +31 -27
- {reader-3.25 → reader-3.27}/src/reader/_update/__init__.py +4 -3
- {reader-3.25 → reader-3.27}/src/reader/_update/base.py +14 -20
- {reader-3.25 → reader-3.27}/src/reader/_update/hooks.py +2 -5
- {reader-3.25 → reader-3.27}/src/reader/_utils.py +17 -37
- {reader-3.25 → reader-3.27}/src/reader/core.py +48 -83
- {reader-3.25 → reader-3.27}/src/reader/discover.py +4 -2
- {reader-3.25 → reader-3.27}/src/reader/exceptions.py +5 -14
- {reader-3.25 → reader-3.27}/src/reader/plugins/__init__.py +1 -8
- {reader-3.25 → reader-3.27}/src/reader/plugins/_loader.py +8 -12
- {reader-3.25 → reader-3.27}/src/reader/plugins/autodiscover.py +7 -16
- {reader-3.25 → reader-3.27}/src/reader/types.py +33 -59
- {reader-3.25 → reader-3.27/src/reader.egg-info}/PKG-INFO +5 -3
- {reader-3.25 → reader-3.27}/src/reader.egg-info/SOURCES.txt +5 -36
- {reader-3.25 → reader-3.27}/src/reader.egg-info/requires.txt +2 -0
- {reader-3.25 → reader-3.27}/tests/fakeparser.py +3 -3
- {reader-3.25 → reader-3.27}/tests/test_lazy_imports.py +4 -4
- {reader-3.25 → reader-3.27}/tests/test_parser.py +93 -44
- {reader-3.25 → reader-3.27}/tests/test_plugins_autodiscover.py +1 -1
- {reader-3.25 → reader-3.27}/tests/test_reader_hooks.py +3 -4
- {reader-3.25 → reader-3.27}/tests/test_reader_lifecycle.py +26 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_private.py +1 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_update.py +29 -76
- {reader-3.25 → reader-3.27}/tests/test_storage.py +16 -0
- {reader-3.25 → reader-3.27}/tests/test_types.py +16 -0
- {reader-3.25 → reader-3.27}/tox.ini +5 -4
- reader-3.25/scripts/backup.sh +0 -36
- reader-3.25/src/reader/_vendor/feedparser/__init__.py +0 -88
- reader-3.25/src/reader/_vendor/feedparser/api.py +0 -377
- reader-3.25/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -73
- reader-3.25/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -80
- reader-3.25/src/reader/_vendor/feedparser/datetimes/greek.py +0 -90
- reader-3.25/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -66
- reader-3.25/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -160
- reader-3.25/src/reader/_vendor/feedparser/datetimes/korean.py +0 -94
- reader-3.25/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -63
- reader-3.25/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -179
- reader-3.25/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -128
- reader-3.25/src/reader/_vendor/feedparser/encodings.py +0 -649
- reader-3.25/src/reader/_vendor/feedparser/exceptions.py +0 -55
- reader-3.25/src/reader/_vendor/feedparser/html.py +0 -352
- reader-3.25/src/reader/_vendor/feedparser/http.py +0 -78
- reader-3.25/src/reader/_vendor/feedparser/mixin.py +0 -842
- reader-3.25/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- reader-3.25/src/reader/_vendor/feedparser/namespaces/_base.py +0 -547
- reader-3.25/src/reader/_vendor/feedparser/namespaces/admin.py +0 -53
- reader-3.25/src/reader/_vendor/feedparser/namespaces/cc.py +0 -70
- reader-3.25/src/reader/_vendor/feedparser/namespaces/dc.py +0 -138
- reader-3.25/src/reader/_vendor/feedparser/namespaces/georss.py +0 -683
- reader-3.25/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -113
- reader-3.25/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -142
- reader-3.25/src/reader/_vendor/feedparser/namespaces/psc.py +0 -74
- reader-3.25/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- reader-3.25/src/reader/_vendor/feedparser/parsers/json.py +0 -135
- reader-3.25/src/reader/_vendor/feedparser/parsers/loose.py +0 -75
- reader-3.25/src/reader/_vendor/feedparser/parsers/strict.py +0 -141
- reader-3.25/src/reader/_vendor/feedparser/sanitizer.py +0 -974
- reader-3.25/src/reader/_vendor/feedparser/sgml.py +0 -98
- reader-3.25/src/reader/_vendor/feedparser/urls.py +0 -174
- reader-3.25/src/reader/_vendor/feedparser/util.py +0 -158
- reader-3.25/src/reader/py.typed +0 -0
- reader-3.25/tests/_plugins/__init__.py +0 -0
- {reader-3.25 → reader-3.27}/LICENSE +0 -0
- {reader-3.25 → reader-3.27}/MANIFEST.in +0 -0
- {reader-3.25 → reader-3.27}/README.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/changelog.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/cli.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/config.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/contributing.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/dev.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/index.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/plugins.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/empty.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/entries-filters-light.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/entries.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/entry-image-light.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/entry.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/feed.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/feeds.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/help.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/import-result.png +0 -0
- {reader-3.25 → reader-3.27}/docs/screenshots/import-select.png +0 -0
- {reader-3.25 → reader-3.27}/docs/tutorial.rst +0 -0
- {reader-3.25 → reader-3.27}/docs/why.rst +0 -0
- {reader-3.25 → reader-3.27}/examples/config.toml +0 -0
- {reader-3.25 → reader-3.27}/examples/custom_headers.py +0 -0
- {reader-3.25 → reader-3.27}/examples/feed_slugs.py +0 -0
- {reader-3.25 → reader-3.27}/examples/parser_only.py +0 -0
- {reader-3.25 → reader-3.27}/examples/podcast.py +0 -0
- {reader-3.25 → reader-3.27}/examples/terminal.py +0 -0
- {reader-3.25 → reader-3.27}/run.sh +0 -0
- {reader-3.25 → reader-3.27}/scripts/bench.py +0 -0
- {reader-3.25 → reader-3.27}/scripts/debug_storage_stats.py +0 -0
- {reader-3.25 → reader-3.27}/scripts/entry_dedupe_backtest.py +0 -0
- {reader-3.25 → reader-3.27}/scripts/jscontrols.html +0 -0
- {reader-3.25 → reader-3.27}/scripts/jscontrols.py +0 -0
- {reader-3.25 → reader-3.27}/scripts/lines.sh +0 -0
- {reader-3.25 → reader-3.27}/scripts/vendor_app_stuff.py +0 -0
- {reader-3.25 → reader-3.27}/setup.cfg +0 -0
- {reader-3.25 → reader-3.27}/src/reader/__main__.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/forms.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/__init__.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/api_thing.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/static/controls.js +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/static/style.css +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/add_entry.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/entries.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/entry.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/feeds.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/layout.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/macros.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/metadata.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/tags.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/legacy/wsgi.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/theme.js +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/README +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/add_feed.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/change_feed_title.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/delete_feed.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/entry.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/feeds.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/help.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/import_feeds.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_app/wsgi.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_config_utils.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_hash_utils.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_logging.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_parser/_http_utils.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/__init__.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/preview_feed_list.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/share.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/sqlite_releases.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_plugins/timer.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/_storage/_schema.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/opml.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/plugins/entry_dedupe.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/plugins/mark_as_read.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/plugins/readtime.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader/plugins/ua_fallback.py +0 -0
- {reader-3.25/src/reader/_vendor/feedparser → reader-3.27/src/reader}/py.typed +0 -0
- {reader-3.25 → reader-3.27}/src/reader/utils.py +0 -0
- {reader-3.25 → reader-3.27}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.25 → reader-3.27}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.25/src/reader/_vendor → reader-3.27/tests/_plugins}/__init__.py +0 -0
- {reader-3.25 → reader-3.27}/tests/_plugins/builtin.py +0 -0
- {reader-3.25 → reader-3.27}/tests/_plugins/error_during_import.py +0 -0
- {reader-3.25 → reader-3.27}/tests/_plugins/error_import_error.py +0 -0
- {reader-3.25 → reader-3.27}/tests/conftest.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/10.json +0 -0
- {reader-3.25 → reader-3.27}/tests/data/10.json.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/custom +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.atom +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.atom.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.json +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.json.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.rss +0 -0
- {reader-3.25 → reader-3.27}/tests/data/empty.rss.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.atom +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.atom.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.json +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.json.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.rss +0 -0
- {reader-3.25 → reader-3.27}/tests/data/full.rss.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/invalid.json +0 -0
- {reader-3.25 → reader-3.27}/tests/data/invalid.json.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/relative.atom +0 -0
- {reader-3.25 → reader-3.27}/tests/data/relative.atom.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/relative.rss +0 -0
- {reader-3.25 → reader-3.27}/tests/data/relative.rss.py +0 -0
- {reader-3.25 → reader-3.27}/tests/data/sqlite_releases.html +0 -0
- {reader-3.25 → reader-3.27}/tests/data/unknown.json +0 -0
- {reader-3.25 → reader-3.27}/tests/data/unknown.json.py +0 -0
- {reader-3.25 → reader-3.27}/tests/fixtures.py +0 -0
- {reader-3.25 → reader-3.27}/tests/reader_methods.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test__types.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test__utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_app_legacy.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_app_wsgi.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_bench.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_changes.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_cli.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_config_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_discover.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_exceptions.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_hash_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_html_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_logging.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_opml.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_entry_dedupe.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_loader.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_mark_as_read.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_preview_feed_list.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_readtime.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_counts.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_deprecations.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_filter.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_integration.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_search.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_sort.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_reader_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_search.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_sql_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_sqlite_utils.py +0 -0
- {reader-3.25 → reader-3.27}/tests/test_tags.py +0 -0
- {reader-3.25 → reader-3.27}/tests/utils.py +0 -0
|
@@ -6,6 +6,41 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.27
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2026-10-11
|
|
13
|
+
|
|
14
|
+
* Add the ``migrate`` :func:`make_reader` argument.
|
|
15
|
+
Thanks to `Divyanshi Purohit`_ for the PR.
|
|
16
|
+
(:issue:`403`)
|
|
17
|
+
* Add :attr:`~EntryCounts.unread` property to
|
|
18
|
+
:class:`EntryCounts` and :class:`EntrySearchCounts`.
|
|
19
|
+
(:issue:`283`)
|
|
20
|
+
|
|
21
|
+
* Support Python 3.15. (:issue:`418`)
|
|
22
|
+
* Support PyPy 3.12. (:issue:`419`)
|
|
23
|
+
* Drop Python 3.11 support. (:issue:`419`)
|
|
24
|
+
|
|
25
|
+
* Stop vendoring `feedparser`_ and
|
|
26
|
+
:ref:`recommend installing it from GitHub instead <no-vendored-feedparser>`.
|
|
27
|
+
* Pin GitHub Actions and add zizmor to CI.
|
|
28
|
+
|
|
29
|
+
.. _Divyanshi Purohit: https://github.com/divyanshi555
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Version 3.26
|
|
33
|
+
------------
|
|
34
|
+
|
|
35
|
+
Released 2026-06-24
|
|
36
|
+
|
|
37
|
+
* Allow exporting the entire database (e.g. for backup or migration).
|
|
38
|
+
|
|
39
|
+
* Through the :meth:`~reader._types.StorageType.export` storage method.
|
|
40
|
+
* In the web app, on the settings page; :ref:`screenshots <screenshots-export>`.
|
|
41
|
+
* In the CLI, as a new ``export`` command.
|
|
42
|
+
|
|
43
|
+
|
|
9
44
|
Version 3.25
|
|
10
45
|
------------
|
|
11
46
|
|
|
@@ -15,7 +50,8 @@ Released 2026-05-25
|
|
|
15
50
|
|
|
16
51
|
* Add :mod:`~reader.plugins.autodiscover` built-in plugin.
|
|
17
52
|
* Add :mod:`reader.discover` module providing low level discovery.
|
|
18
|
-
* In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
|
|
53
|
+
* In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
|
|
54
|
+
when adding a feed; :ref:`screenshots <screenshots-autodiscovery>`.
|
|
19
55
|
|
|
20
56
|
* Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
|
|
21
57
|
|
|
@@ -579,7 +615,7 @@ Released 2023-08-20
|
|
|
579
615
|
|
|
580
616
|
* Make :exc:`ReaderWarning` inherit from :exc:`ReaderError`.
|
|
581
617
|
|
|
582
|
-
* Include a diagram of the :ref:`exctree
|
|
618
|
+
* Include a diagram of the :ref:`exception hierarchy <exctree>` in the :doc:`api`.
|
|
583
619
|
|
|
584
620
|
* Add werkzeug dependency,
|
|
585
621
|
instead of vendoring selected :mod:`werkzeug.http` utilities.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.27
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -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.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
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.12
|
|
36
36
|
Description-Content-Type: text/x-rst
|
|
37
37
|
License-File: LICENSE
|
|
38
38
|
Requires-Dist: typing-extensions>=4
|
|
@@ -49,6 +49,8 @@ Provides-Extra: app
|
|
|
49
49
|
Requires-Dist: flask>=0.10; extra == "app"
|
|
50
50
|
Requires-Dist: humanize>=4; extra == "app"
|
|
51
51
|
Requires-Dist: PyYAML; extra == "app"
|
|
52
|
+
Requires-Dist: click; extra == "app"
|
|
53
|
+
Requires-Dist: platformdirs; extra == "app"
|
|
52
54
|
Requires-Dist: WTForms; extra == "app"
|
|
53
55
|
Requires-Dist: Flask-WTF; extra == "app"
|
|
54
56
|
Requires-Dist: jinja2-fragments; extra == "app"
|
|
@@ -60,9 +60,11 @@ Data objects
|
|
|
60
60
|
|
|
61
61
|
.. autoclass:: EntryCounts
|
|
62
62
|
:members:
|
|
63
|
+
:inherited-members:
|
|
63
64
|
|
|
64
65
|
.. autoclass:: EntrySearchCounts
|
|
65
66
|
:members:
|
|
67
|
+
:inherited-members:
|
|
66
68
|
|
|
67
69
|
.. autoclass:: UpdateResult
|
|
68
70
|
:members:
|
|
@@ -85,9 +87,16 @@ Data objects
|
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
.. _exctree:
|
|
91
|
+
|
|
88
92
|
Exceptions
|
|
89
93
|
----------
|
|
90
94
|
|
|
95
|
+
The class hierarchy for :mod:`reader` exceptions is:
|
|
96
|
+
|
|
97
|
+
.. classtree:: ReaderError
|
|
98
|
+
|
|
99
|
+
|
|
91
100
|
.. autoexception:: ReaderError
|
|
92
101
|
:members:
|
|
93
102
|
|
|
@@ -180,16 +189,6 @@ Exceptions
|
|
|
180
189
|
:show-inheritance:
|
|
181
190
|
|
|
182
191
|
|
|
183
|
-
.. _exctree:
|
|
184
|
-
|
|
185
|
-
Exception hierarchy
|
|
186
|
-
~~~~~~~~~~~~~~~~~~~
|
|
187
|
-
|
|
188
|
-
The class hierarchy for :mod:`reader` exceptions is:
|
|
189
|
-
|
|
190
|
-
.. classtree:: ReaderError
|
|
191
|
-
|
|
192
|
-
|
|
193
192
|
|
|
194
193
|
Enumerations
|
|
195
194
|
------------
|
|
@@ -19,6 +19,8 @@ Features, as of *reader* version |version|:
|
|
|
19
19
|
* article view
|
|
20
20
|
* custom feed titles
|
|
21
21
|
* dark mode
|
|
22
|
+
* feed autodiscovery
|
|
23
|
+
* full data export
|
|
22
24
|
|
|
23
25
|
More features already implemented in the library
|
|
24
26
|
and the :ref:`legacy web app <legacy web app>`
|
|
@@ -86,6 +88,20 @@ Screenshots
|
|
|
86
88
|
|
|
87
89
|
contextual help
|
|
88
90
|
|
|
91
|
+
.. _screenshots-autodiscovery:
|
|
92
|
+
|
|
93
|
+
.. figure:: screenshots/autodiscovery.png
|
|
94
|
+
:width: 240px
|
|
95
|
+
|
|
96
|
+
feed autodiscovery
|
|
97
|
+
|
|
98
|
+
.. _screenshots-export:
|
|
99
|
+
|
|
100
|
+
.. figure:: screenshots/export-database.png
|
|
101
|
+
:width: 240px
|
|
102
|
+
|
|
103
|
+
database exports
|
|
104
|
+
|
|
89
105
|
|
|
90
106
|
Serving the web app
|
|
91
107
|
-------------------
|
|
@@ -78,7 +78,7 @@ autodoc_typehints_description_target = 'documented'
|
|
|
78
78
|
autodoc_type_aliases = {
|
|
79
79
|
'ResourceInput': 'reader.types.ResourceInput',
|
|
80
80
|
'AnyResourceInput': 'reader.types.AnyResourceInput',
|
|
81
|
-
'
|
|
81
|
+
'JSON': 'reader.types.JSON',
|
|
82
82
|
'TagFilter': 'reader._types.TagFilter',
|
|
83
83
|
'TristateFilter': 'reader._types.TristateFilter',
|
|
84
84
|
}
|
|
@@ -226,5 +226,19 @@ def class_tree(cls):
|
|
|
226
226
|
return '\n'.join(output(classes)) + '\n'
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
import builtins
|
|
230
|
+
|
|
231
|
+
std_dir = builtins.dir
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def source_order_dir(obj):
|
|
235
|
+
if isinstance(obj, type):
|
|
236
|
+
return [name for cls in obj.mro() for name in cls.__dict__.keys()]
|
|
237
|
+
return std_dir(obj)
|
|
238
|
+
|
|
239
|
+
|
|
229
240
|
def setup(app):
|
|
230
241
|
app.add_directive("classtree", ClassTree)
|
|
242
|
+
|
|
243
|
+
# TODO: remove when https://github.com/sphinx-doc/sphinx/issues/628 is resolved
|
|
244
|
+
builtins.dir = source_order_dir
|
|
@@ -137,17 +137,30 @@ since each connection would be to a *different* database::
|
|
|
137
137
|
.. _temporary: https://sqlite.org/inmemorydb.html#temp_db
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
Migrations
|
|
141
|
+
~~~~~~~~~~
|
|
142
|
+
|
|
143
|
+
Schema migrations depend on the storage used.
|
|
144
|
+
|
|
145
|
+
For the default SQLite storage,
|
|
146
|
+
migrations happen automatically during :func:`make_reader`,
|
|
147
|
+
unless you pass ``migrate=False``.
|
|
148
|
+
|
|
149
|
+
|
|
140
150
|
.. _backups:
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
Backups
|
|
153
|
+
~~~~~~~
|
|
154
|
+
|
|
155
|
+
Making backups generally depends on the storage used.
|
|
144
156
|
|
|
145
|
-
|
|
157
|
+
If the storage implements it, you can use
|
|
158
|
+
the :meth:`~reader._types.StorageType.export` method
|
|
159
|
+
or the ``export`` :doc:`CLI <cli>` command.
|
|
146
160
|
|
|
147
|
-
For the SQLite storage, you
|
|
148
|
-
the
|
|
149
|
-
or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_
|
|
150
|
-
(see :gh:`backup.sh <scripts/backup.sh>` for an example).
|
|
161
|
+
For the SQLite storage, you can also use
|
|
162
|
+
the `.backup command <https://sqlite.org/cli.html>`_
|
|
163
|
+
or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_.
|
|
151
164
|
|
|
152
165
|
|
|
153
166
|
|
|
@@ -28,14 +28,12 @@ These packages will be installed automatically when installing *reader*:
|
|
|
28
28
|
|
|
29
29
|
.. note::
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.. _develop: https://github.com/kurtmckee/feedparser
|
|
31
|
+
The `feedparser`_ ``main`` branch contains a `change`_
|
|
32
|
+
that decreases memory usage by ~35% during feed updates;
|
|
33
|
+
as of feedparser 6.0.14, this change is not yet included in PyPI releases;
|
|
34
|
+
to benefit from it, install feedparser directly from GitHub.
|
|
35
|
+
|
|
36
|
+
.. _change: https://github.com/kurtmckee/feedparser/pull/302
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
.. _optional dependencies:
|
|
Binary file
|
|
Binary file
|
|
@@ -19,10 +19,10 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
21
|
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
-
"Programming Language :: Python :: 3.11",
|
|
23
22
|
"Programming Language :: Python :: 3.12",
|
|
24
23
|
"Programming Language :: Python :: 3.13",
|
|
25
24
|
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3.15",
|
|
26
26
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
27
27
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
28
28
|
"Topic :: Internet",
|
|
@@ -33,7 +33,7 @@ classifiers = [
|
|
|
33
33
|
"Topic :: Utilities",
|
|
34
34
|
"Typing :: Typed",
|
|
35
35
|
]
|
|
36
|
-
requires-python = ">=3.
|
|
36
|
+
requires-python = ">=3.12"
|
|
37
37
|
dependencies = [
|
|
38
38
|
"typing-extensions>=4",
|
|
39
39
|
"feedparser>=6",
|
|
@@ -65,8 +65,11 @@ cli = [
|
|
|
65
65
|
app = [
|
|
66
66
|
"flask>=0.10",
|
|
67
67
|
"humanize>=4",
|
|
68
|
-
# for
|
|
68
|
+
# for legacy (tags)
|
|
69
69
|
"PyYAML",
|
|
70
|
+
# for cli
|
|
71
|
+
"click",
|
|
72
|
+
"platformdirs",
|
|
70
73
|
# for v2
|
|
71
74
|
"WTForms",
|
|
72
75
|
"Flask-WTF",
|
|
@@ -173,6 +176,9 @@ files = ["src"]
|
|
|
173
176
|
show_error_codes = true
|
|
174
177
|
pretty = true
|
|
175
178
|
strict = true
|
|
179
|
+
strict_equality_for_none = true
|
|
180
|
+
warn_unreachable = true
|
|
181
|
+
|
|
176
182
|
[[tool.mypy.overrides]]
|
|
177
183
|
module = [
|
|
178
184
|
"reader.__main__",
|
|
@@ -187,7 +193,6 @@ module = [
|
|
|
187
193
|
"reader.plugins.readtime",
|
|
188
194
|
"reader.plugins.mark_as_read",
|
|
189
195
|
"reader.plugins.autodiscover",
|
|
190
|
-
"reader._vendor.feedparser.*",
|
|
191
196
|
]
|
|
192
197
|
ignore_errors = true
|
|
193
198
|
|
|
@@ -20,18 +20,8 @@ def run(*args, **kwargs):
|
|
|
20
20
|
return subprocess.run(*args, check=True, **kwargs)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def run_no_venv(*args, **kwargs):
|
|
24
|
-
env = os.environ.copy()
|
|
25
|
-
virtual_env = env.pop('VIRTUAL_ENV', None)
|
|
26
|
-
if virtual_env:
|
|
27
|
-
env['PATH'] = ':'.join(
|
|
28
|
-
p for p in env['PATH'].split(':') if not p.startswith(virtual_env)
|
|
29
|
-
)
|
|
30
|
-
return run(*args, env=env, **kwargs)
|
|
31
|
-
|
|
32
|
-
|
|
33
23
|
def run_tox():
|
|
34
|
-
|
|
24
|
+
run('tox -p all', shell=True)
|
|
35
25
|
|
|
36
26
|
|
|
37
27
|
def path_sub(pattern, repl, path):
|
|
@@ -19,6 +19,7 @@ from flask import render_template
|
|
|
19
19
|
from flask import render_template_string
|
|
20
20
|
from flask import request
|
|
21
21
|
from flask import Response
|
|
22
|
+
from flask import send_from_directory
|
|
22
23
|
from flask import stream_with_context
|
|
23
24
|
from flask import url_for
|
|
24
25
|
from flask_wtf.csrf import CSRFError
|
|
@@ -35,6 +36,9 @@ from reader import InvalidFeedURLError
|
|
|
35
36
|
from reader import opml
|
|
36
37
|
from reader import UpdateError
|
|
37
38
|
|
|
39
|
+
from .exports import ExportNotFoundError
|
|
40
|
+
from .exports import TooManyExportsError
|
|
41
|
+
from .ext import get_exports
|
|
38
42
|
from .ext import get_reader
|
|
39
43
|
from .ext import ReaderExtension
|
|
40
44
|
from .forms import AddFeed
|
|
@@ -94,7 +98,7 @@ def entries():
|
|
|
94
98
|
hostname = urlparse(feed.url).hostname
|
|
95
99
|
autodiscover = [
|
|
96
100
|
(link, hostname and urlparse(link['href']).hostname == hostname)
|
|
97
|
-
for link in reader.get_tag(feed, autodiscover_tag,
|
|
101
|
+
for link in reader.get_tag(feed, autodiscover_tag, ())
|
|
98
102
|
]
|
|
99
103
|
|
|
100
104
|
kwargs = dict(form.data)
|
|
@@ -356,6 +360,38 @@ def help():
|
|
|
356
360
|
return render_template('help.html')
|
|
357
361
|
|
|
358
362
|
|
|
363
|
+
@blueprint.route('/settings')
|
|
364
|
+
def settings():
|
|
365
|
+
return render_template('settings.html', exports=get_exports().list())
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@blueprint.route('/settings/exports', methods=['POST'])
|
|
369
|
+
def create_export():
|
|
370
|
+
try:
|
|
371
|
+
get_exports().create()
|
|
372
|
+
except TooManyExportsError:
|
|
373
|
+
flash("Too many exports, delete some of them first.", 'exports.danger')
|
|
374
|
+
return redirect(url_for('.settings', _anchor='exports'), code=303)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@blueprint.route('/settings/exports/<name>')
|
|
378
|
+
def get_export(name):
|
|
379
|
+
exports = get_exports()
|
|
380
|
+
if not exports.exists(name):
|
|
381
|
+
abort(404)
|
|
382
|
+
return send_from_directory(exports.path, name, as_attachment=True)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@blueprint.route('/settings/exports/<name>/delete', methods=['POST'])
|
|
386
|
+
def delete_export(name):
|
|
387
|
+
# flask should ensure <name> doesn't contain slashes etc.
|
|
388
|
+
try:
|
|
389
|
+
get_exports().delete(name)
|
|
390
|
+
except ExportNotFoundError:
|
|
391
|
+
abort(404)
|
|
392
|
+
return redirect(url_for('.settings', _anchor='exports'), code=303)
|
|
393
|
+
|
|
394
|
+
|
|
359
395
|
def stream_template(template_name_or_list, **kwargs):
|
|
360
396
|
# Ensure flashed messages get removed from the session,
|
|
361
397
|
# otherwise they keep adding up and never disappear.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
1
3
|
import click
|
|
4
|
+
import platformdirs
|
|
2
5
|
|
|
6
|
+
from reader._cli import app_name
|
|
3
7
|
from reader._cli import load_reader_config_from_context
|
|
4
8
|
from reader._cli import setup_logging
|
|
5
9
|
|
|
@@ -26,9 +30,16 @@ def make_add_response_headers_middleware(wsgi_app, headers):
|
|
|
26
30
|
help="Import path to a web app plug-in. Can be passed multiple times.",
|
|
27
31
|
)
|
|
28
32
|
@click.option('--legacy/--no-legacy', help="Serve the legacy app.")
|
|
33
|
+
@click.option(
|
|
34
|
+
'--cache-dir',
|
|
35
|
+
type=click.Path(file_okay=False, path_type=pathlib.Path),
|
|
36
|
+
default=platformdirs.user_cache_dir(app_name),
|
|
37
|
+
show_default=True,
|
|
38
|
+
help="The directory to store cache data in.",
|
|
39
|
+
)
|
|
29
40
|
@click.option('-v', '--verbose', count=True)
|
|
30
41
|
@click.pass_context
|
|
31
|
-
def cli(ctx, host, port, plugins, legacy, verbose):
|
|
42
|
+
def cli(ctx, host, port, plugins, legacy, cache_dir, verbose):
|
|
32
43
|
"""Run a local HTTP reader server."""
|
|
33
44
|
|
|
34
45
|
setup_logging(verbose)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from datetime import timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TooManyExportsError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExportNotFoundError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Exports:
|
|
15
|
+
|
|
16
|
+
max_files = 2
|
|
17
|
+
prefix_fmt = "reader.%Y-%m-%d-%H-%M-%S"
|
|
18
|
+
|
|
19
|
+
def __init__(self, reader, path):
|
|
20
|
+
self.reader = reader
|
|
21
|
+
self.path = pathlib.Path(path)
|
|
22
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def parse_name(self, name):
|
|
25
|
+
name_fmt = self.reader._storage.export_name(self.prefix_fmt)
|
|
26
|
+
try:
|
|
27
|
+
dt = datetime.strptime(name, name_fmt)
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
else:
|
|
31
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
32
|
+
|
|
33
|
+
def create(self):
|
|
34
|
+
# not looking at file names accounts for exports in progress
|
|
35
|
+
if len(list(self.path.iterdir())) >= self.max_files:
|
|
36
|
+
raise TooManyExportsError
|
|
37
|
+
prefix = self.reader._now().strftime(self.prefix_fmt)
|
|
38
|
+
self.reader._storage.export(self.path, prefix)
|
|
39
|
+
|
|
40
|
+
def exists(self, name):
|
|
41
|
+
if not self.parse_name(name):
|
|
42
|
+
return False
|
|
43
|
+
return (self.path / name).exists()
|
|
44
|
+
|
|
45
|
+
def list(self):
|
|
46
|
+
rv = []
|
|
47
|
+
for path in self.path.iterdir():
|
|
48
|
+
if dt := self.parse_name(path.name):
|
|
49
|
+
rv.append((path, dt))
|
|
50
|
+
return rv
|
|
51
|
+
|
|
52
|
+
def delete(self, name):
|
|
53
|
+
if not self.parse_name(name):
|
|
54
|
+
raise ExportNotFoundError
|
|
55
|
+
try:
|
|
56
|
+
(self.path / name).unlink()
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
raise ExportNotFoundError from None
|
|
@@ -3,6 +3,7 @@ Flask extension that encapsulates configurable reader-related aspects.
|
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import pathlib
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
|
|
8
9
|
from flask import current_app
|
|
@@ -10,13 +11,20 @@ from flask import current_app
|
|
|
10
11
|
from reader import make_reader
|
|
11
12
|
from reader.plugins._loader import PluginLoader
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
from .exports import Exports
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def get_reader():
|
|
17
18
|
return current_app.extensions['reader'].get_reader()
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def get_exports():
|
|
22
|
+
return current_app.extensions['reader'].get_exports()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_plugin_loader = PluginLoader('init_app')
|
|
26
|
+
|
|
27
|
+
|
|
20
28
|
class ReaderExtension:
|
|
21
29
|
|
|
22
30
|
def __init__(self, app=None, state_cls=None):
|
|
@@ -26,26 +34,45 @@ class ReaderExtension:
|
|
|
26
34
|
|
|
27
35
|
def init_app(self, app):
|
|
28
36
|
config = app.config['READER']
|
|
29
|
-
app.extensions['reader'] = self.state_cls(
|
|
37
|
+
app.extensions['reader'] = self.state_cls(
|
|
38
|
+
config[''], config.get('web', {}).get('cache_dir')
|
|
39
|
+
)
|
|
30
40
|
_plugin_loader.oneshot(app, config.get('web', {}).get('plugins', ()))
|
|
31
41
|
|
|
32
42
|
|
|
33
43
|
@dataclass
|
|
34
44
|
class ReaderStateBase:
|
|
35
45
|
args: dict
|
|
46
|
+
cache_dir: pathlib.Path | None
|
|
36
47
|
|
|
37
48
|
def get_reader(self):
|
|
38
49
|
raise NotImplementedError
|
|
39
50
|
|
|
51
|
+
def get_user_cache_dir(self):
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
40
54
|
def _make_reader(self, **kwargs):
|
|
41
55
|
return make_reader(**self.args | kwargs)
|
|
42
56
|
|
|
57
|
+
def get_exports(self):
|
|
58
|
+
cache_dir = self.get_user_cache_dir()
|
|
59
|
+
if not cache_dir:
|
|
60
|
+
return None
|
|
61
|
+
return Exports(self.get_reader(), cache_dir / 'exports')
|
|
62
|
+
|
|
43
63
|
|
|
44
64
|
class _ReaderState(ReaderStateBase):
|
|
45
65
|
|
|
46
66
|
def __init__(self, *args, **kwargs):
|
|
47
67
|
super().__init__(*args, **kwargs)
|
|
48
68
|
self._reader = self._make_reader()
|
|
69
|
+
self._exports = super().get_exports()
|
|
49
70
|
|
|
50
71
|
def get_reader(self):
|
|
51
72
|
return self._reader
|
|
73
|
+
|
|
74
|
+
def get_user_cache_dir(self):
|
|
75
|
+
return self.cache_dir
|
|
76
|
+
|
|
77
|
+
def get_exports(self):
|
|
78
|
+
return self._exports
|
|
@@ -69,8 +69,6 @@
|
|
|
69
69
|
height: auto;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
:
|
|
74
|
-
outline: 0;
|
|
75
|
-
box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color);
|
|
72
|
+
h1:target, h2:target, h3:target, h4:target, h5:target, h6:target {
|
|
73
|
+
text-decoration: underline;
|
|
76
74
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
{{ macros.flashed_messages() }}
|
|
20
20
|
|
|
21
21
|
{%- if feed and not feed.url.startswith('reader:') %}
|
|
22
|
-
<ul class="list-unstyled
|
|
22
|
+
<ul class="list-unstyled">
|
|
23
23
|
|
|
24
24
|
{%- if not feed.last_updated and not feed.last_exception %}
|
|
25
25
|
<li class="alert alert-secondary">
|
|
@@ -62,10 +62,13 @@
|
|
|
62
62
|
<ul class="navbar-nav mb-0">
|
|
63
63
|
{% block right_hidden_navbar %}
|
|
64
64
|
|
|
65
|
+
{% block right_hidden_navbar_common %}
|
|
66
|
+
|
|
65
67
|
<li class="nav-item">
|
|
66
68
|
<a
|
|
67
69
|
href="{{ url_for('reader.help', _anchor=help_topic | default(none)) }}"
|
|
68
70
|
class="btn btn-link nav-link d-flex align-items-center"
|
|
71
|
+
title="help"
|
|
69
72
|
>
|
|
70
73
|
<i class="bi bi-question-lg"></i>
|
|
71
74
|
<span class="d-md-none ms-2">help</span>
|
|
@@ -78,7 +81,7 @@
|
|
|
78
81
|
type="button" id="theme"
|
|
79
82
|
data-bs-toggle="dropdown" data-bs-display="static"
|
|
80
83
|
aria-expanded="false" aria-label="toggle theme (auto)">
|
|
81
|
-
<i class="bi bi-circle-half theme-icon-active"></i>
|
|
84
|
+
<i class="bi bi-circle-half theme-icon-active" title="toggle theme"></i>
|
|
82
85
|
<span class="d-md-none ms-2" id="theme-text">toggle theme</span>
|
|
83
86
|
</button>
|
|
84
87
|
<ul class="dropdown-menu dropdown-menu-end mb-2" aria-labelledby="theme-text">
|
|
@@ -110,6 +113,19 @@
|
|
|
110
113
|
</li>
|
|
111
114
|
|
|
112
115
|
{% endblock %}
|
|
116
|
+
|
|
117
|
+
<li class="nav-item">
|
|
118
|
+
<a
|
|
119
|
+
href="{{ url_for('reader.settings') }}"
|
|
120
|
+
class="btn btn-link nav-link d-flex align-items-center"
|
|
121
|
+
title="settings"
|
|
122
|
+
>
|
|
123
|
+
<i class="bi bi-gear-fill"></i>
|
|
124
|
+
<span class="d-md-none ms-2">settings</span>
|
|
125
|
+
</a>
|
|
126
|
+
</li>
|
|
127
|
+
|
|
128
|
+
{% endblock %}
|
|
113
129
|
</ul>
|
|
114
130
|
</div>
|
|
115
131
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
{% macro flashed_messages() %}
|
|
3
|
-
{%-
|
|
2
|
+
{% macro flashed_messages(category_filter=()) %}
|
|
3
|
+
{%- set messages = get_flashed_messages(True, category_filter) %}
|
|
4
|
+
{%- if messages %}
|
|
4
5
|
<ul class="list-unstyled">
|
|
5
|
-
{%- for category, message in
|
|
6
|
-
<li class="alert alert-{{ category }}">{{ message }}</li>
|
|
6
|
+
{%- for category, message in messages %}
|
|
7
|
+
<li class="alert alert-{{ category.rpartition('.')[2] }}">{{ message }}</li>
|
|
7
8
|
{%- endfor %}
|
|
8
9
|
</ul>
|
|
9
10
|
{%- endif %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% extends "settings_base.html" %}
|