reader 3.25__tar.gz → 3.26__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.26}/CHANGES.rst +14 -1
- {reader-3.25/src/reader.egg-info → reader-3.26}/PKG-INFO +3 -1
- {reader-3.25 → reader-3.26}/docs/app.rst +16 -0
- {reader-3.25 → reader-3.26}/docs/guide.rst +10 -7
- reader-3.26/docs/screenshots/autodiscovery.png +0 -0
- reader-3.26/docs/screenshots/export-database.png +0 -0
- {reader-3.25 → reader-3.26}/pyproject.toml +4 -1
- {reader-3.25 → reader-3.26}/src/reader/__init__.py +1 -1
- {reader-3.25 → reader-3.26}/src/reader/_app/__init__.py +37 -1
- {reader-3.25 → reader-3.26}/src/reader/_app/cli.py +12 -1
- reader-3.26/src/reader/_app/exports.py +58 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/ext.py +29 -2
- {reader-3.25 → reader-3.26}/src/reader/_app/static/style.css +2 -4
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/entries.html +1 -1
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/layout_base.html +17 -1
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/macros.html +5 -4
- reader-3.26/src/reader/_app/templates/settings.html +1 -0
- reader-3.26/src/reader/_app/templates/settings_base.html +67 -0
- {reader-3.25 → reader-3.26}/src/reader/_cli.py +12 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_base.py +31 -1
- {reader-3.25 → reader-3.26}/src/reader/_types.py +14 -1
- {reader-3.25 → reader-3.26/src/reader.egg-info}/PKG-INFO +3 -1
- {reader-3.25 → reader-3.26}/src/reader.egg-info/SOURCES.txt +5 -1
- {reader-3.25 → reader-3.26}/src/reader.egg-info/requires.txt +2 -0
- {reader-3.25 → reader-3.26}/tests/test_storage.py +16 -0
- reader-3.25/scripts/backup.sh +0 -36
- {reader-3.25 → reader-3.26}/LICENSE +0 -0
- {reader-3.25 → reader-3.26}/MANIFEST.in +0 -0
- {reader-3.25 → reader-3.26}/README.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/api.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/changelog.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/cli.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/conf.py +0 -0
- {reader-3.25 → reader-3.26}/docs/config.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/contributing.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/dev.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/index.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/install.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/internal.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/plugins.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/empty.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/entries-filters-light.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/entries.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/entry-image-light.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/entry.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/feed.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/feeds.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/help.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/import-result.png +0 -0
- {reader-3.25 → reader-3.26}/docs/screenshots/import-select.png +0 -0
- {reader-3.25 → reader-3.26}/docs/tutorial.rst +0 -0
- {reader-3.25 → reader-3.26}/docs/why.rst +0 -0
- {reader-3.25 → reader-3.26}/examples/config.toml +0 -0
- {reader-3.25 → reader-3.26}/examples/custom_headers.py +0 -0
- {reader-3.25 → reader-3.26}/examples/feed_slugs.py +0 -0
- {reader-3.25 → reader-3.26}/examples/parser_only.py +0 -0
- {reader-3.25 → reader-3.26}/examples/podcast.py +0 -0
- {reader-3.25 → reader-3.26}/examples/terminal.py +0 -0
- {reader-3.25 → reader-3.26}/run.sh +0 -0
- {reader-3.25 → reader-3.26}/scripts/bench.py +0 -0
- {reader-3.25 → reader-3.26}/scripts/debug_storage_stats.py +0 -0
- {reader-3.25 → reader-3.26}/scripts/entry_dedupe_backtest.py +0 -0
- {reader-3.25 → reader-3.26}/scripts/jscontrols.html +0 -0
- {reader-3.25 → reader-3.26}/scripts/jscontrols.py +0 -0
- {reader-3.25 → reader-3.26}/scripts/lines.sh +0 -0
- {reader-3.25 → reader-3.26}/scripts/release.py +0 -0
- {reader-3.25 → reader-3.26}/scripts/vendor_app_stuff.py +0 -0
- {reader-3.25 → reader-3.26}/setup.cfg +0 -0
- {reader-3.25 → reader-3.26}/src/reader/__main__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/forms.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/api_thing.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/static/controls.js +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/static/style.css +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/add_entry.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/entries.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/entry.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/feeds.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/layout.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/macros.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/metadata.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/tags.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/legacy/wsgi.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/theme.js +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/README +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/add_feed.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/change_feed_title.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/delete_feed.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/entry.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/feeds.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/help.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/import_feeds.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_app/wsgi.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_config_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_hash_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_logging.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/_http_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/feedparser.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/file.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/http.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_parser/jsonfeed.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/preview_feed_list.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/share.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/sqlite_releases.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_plugins/timer.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_changes.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_entries.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_feeds.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_html_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_schema.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_search.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_sql_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_sqlite_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_storage/_tags.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_update/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_update/base.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_update/hooks.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/core.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/discover.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/exceptions.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/opml.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/_loader.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/autodiscover.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/entry_dedupe.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/mark_as_read.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/readtime.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/plugins/ua_fallback.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/py.typed +0 -0
- {reader-3.25 → reader-3.26}/src/reader/types.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader/utils.py +0 -0
- {reader-3.25 → reader-3.26}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.25 → reader-3.26}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.25 → reader-3.26}/tests/_plugins/__init__.py +0 -0
- {reader-3.25 → reader-3.26}/tests/_plugins/builtin.py +0 -0
- {reader-3.25 → reader-3.26}/tests/_plugins/error_during_import.py +0 -0
- {reader-3.25 → reader-3.26}/tests/_plugins/error_import_error.py +0 -0
- {reader-3.25 → reader-3.26}/tests/conftest.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/10.json +0 -0
- {reader-3.25 → reader-3.26}/tests/data/10.json.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/custom +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.atom +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.atom.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.json +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.json.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.rss +0 -0
- {reader-3.25 → reader-3.26}/tests/data/empty.rss.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.atom +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.atom.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.json +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.json.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.rss +0 -0
- {reader-3.25 → reader-3.26}/tests/data/full.rss.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/invalid.json +0 -0
- {reader-3.25 → reader-3.26}/tests/data/invalid.json.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/relative.atom +0 -0
- {reader-3.25 → reader-3.26}/tests/data/relative.atom.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/relative.rss +0 -0
- {reader-3.25 → reader-3.26}/tests/data/relative.rss.py +0 -0
- {reader-3.25 → reader-3.26}/tests/data/sqlite_releases.html +0 -0
- {reader-3.25 → reader-3.26}/tests/data/unknown.json +0 -0
- {reader-3.25 → reader-3.26}/tests/data/unknown.json.py +0 -0
- {reader-3.25 → reader-3.26}/tests/fakeparser.py +0 -0
- {reader-3.25 → reader-3.26}/tests/fixtures.py +0 -0
- {reader-3.25 → reader-3.26}/tests/reader_methods.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test__types.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test__utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_app_legacy.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_app_wsgi.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_bench.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_changes.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_cli.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_config_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_discover.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_exceptions.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_hash_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_html_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_lazy_imports.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_logging.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_opml.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_parser.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_autodiscover.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_entry_dedupe.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_loader.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_mark_as_read.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_preview_feed_list.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_readtime.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_counts.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_deprecations.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_filter.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_hooks.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_integration.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_lifecycle.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_private.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_search.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_sort.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_update.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_reader_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_search.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_sql_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_sqlite_utils.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_tags.py +0 -0
- {reader-3.25 → reader-3.26}/tests/test_types.py +0 -0
- {reader-3.25 → reader-3.26}/tests/utils.py +0 -0
- {reader-3.25 → reader-3.26}/tox.ini +0 -0
|
@@ -6,6 +6,18 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.26
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2026-06-24
|
|
13
|
+
|
|
14
|
+
* Allow exporting the entire database (e.g. for backup or migration).
|
|
15
|
+
|
|
16
|
+
* Through the :meth:`~reader._types.StorageType.export` storage method.
|
|
17
|
+
* In the web app, on the settings page; :ref:`screenshots <screenshots-export>`.
|
|
18
|
+
* In the CLI, as a new ``export`` command.
|
|
19
|
+
|
|
20
|
+
|
|
9
21
|
Version 3.25
|
|
10
22
|
------------
|
|
11
23
|
|
|
@@ -15,7 +27,8 @@ Released 2026-05-25
|
|
|
15
27
|
|
|
16
28
|
* Add :mod:`~reader.plugins.autodiscover` built-in plugin.
|
|
17
29
|
* Add :mod:`reader.discover` module providing low level discovery.
|
|
18
|
-
* In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
|
|
30
|
+
* In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
|
|
31
|
+
when adding a feed; :ref:`screenshots <screenshots-autodiscovery>`.
|
|
19
32
|
|
|
20
33
|
* Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
|
|
21
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.26
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -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"
|
|
@@ -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
|
-------------------
|
|
@@ -139,15 +139,18 @@ since each connection would be to a *different* database::
|
|
|
139
139
|
|
|
140
140
|
.. _backups:
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
Backups
|
|
143
|
+
~~~~~~~
|
|
144
144
|
|
|
145
|
-
Making
|
|
145
|
+
Making backups generally depends on the storage used.
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
the
|
|
149
|
-
or
|
|
150
|
-
|
|
147
|
+
If the storage implements it, you can use
|
|
148
|
+
the :meth:`~reader._types.StorageType.export` method
|
|
149
|
+
or the ``export`` :doc:`CLI <cli>` command.
|
|
150
|
+
|
|
151
|
+
For the SQLite storage, you can also use
|
|
152
|
+
the `.backup command <https://sqlite.org/cli.html>`_
|
|
153
|
+
or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_.
|
|
151
154
|
|
|
152
155
|
|
|
153
156
|
|
|
Binary file
|
|
Binary file
|
|
@@ -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" %}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
|
|
3
|
+
{% import "macros.html" as macros %}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
{% block page_title %}
|
|
7
|
+
Settings - {{ super() }}
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
{% block body %}
|
|
12
|
+
|
|
13
|
+
<h1>Settings</h1>
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
<h2 id="exports">Export database</h2>
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
Export your entire <em>reader</em> database,
|
|
20
|
+
for backup or use with another installation.
|
|
21
|
+
|
|
22
|
+
{{ macros.flashed_messages(['exports.danger']) }}
|
|
23
|
+
|
|
24
|
+
<div class="card my-3">
|
|
25
|
+
<div class="card-header">
|
|
26
|
+
Recent exports
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<ul class="list-group list-group-flush">
|
|
30
|
+
{%- for path, date in exports | sort(reverse=True) %}
|
|
31
|
+
<li class="list-group-item d-flex flex-wrap justify-content-between align-items-center gap-2">
|
|
32
|
+
<span>
|
|
33
|
+
<a href="{{ url_for('.get_export', name=path.name) }}" class="d-inline-block">
|
|
34
|
+
<time datetime="{{ date }}" title="{{ date }}">
|
|
35
|
+
{{ date | humanize_naturaltime }}
|
|
36
|
+
</time>
|
|
37
|
+
</a>
|
|
38
|
+
<span class="text-secondary">({{ path.stat().st_size | filesizeformat }})</span>
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<form action="{{ url_for('.delete_export', name=path.name) }}" method="post">
|
|
42
|
+
{{ macros.csrf_token_field() }}
|
|
43
|
+
<input type="submit" class="btn btn-outline-danger btn-sm" value="delete">
|
|
44
|
+
</form>
|
|
45
|
+
|
|
46
|
+
</li>
|
|
47
|
+
{%- else %}
|
|
48
|
+
<li class="list-group-item">
|
|
49
|
+
no exports found
|
|
50
|
+
</li>
|
|
51
|
+
{%- endfor %}
|
|
52
|
+
</ul>
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<form action="{{ url_for('.create_export') }}" method="post" class="my-3">
|
|
57
|
+
{{ macros.csrf_token_field() }}
|
|
58
|
+
<input type="submit" class="btn btn-secondary" value="create new export">
|
|
59
|
+
</form>
|
|
60
|
+
|
|
61
|
+
<p class="text-secondary">
|
|
62
|
+
To export feeds as a subscription list, click the
|
|
63
|
+
<i class="bi bi-filetype-xml" aria-label="export as OPML"></i> icon
|
|
64
|
+
on the <a href="{{ url_for('.feeds') }}">Feeds</a> page.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
{% endblock %}
|
|
@@ -459,6 +459,18 @@ def search_entries(reader, query):
|
|
|
459
459
|
click.echo(f"{entry.feed.url} {entry.link or entry.id}")
|
|
460
460
|
|
|
461
461
|
|
|
462
|
+
@cli.command()
|
|
463
|
+
@click.argument('outdir', type=click.Path(exists=True, file_okay=False), default='.')
|
|
464
|
+
@click.option('--prefix', default="reader.%Y-%m-%d-%H-%M-%S", show_default=True)
|
|
465
|
+
@pass_reader
|
|
466
|
+
def export(reader, outdir, prefix):
|
|
467
|
+
"""Export all data to a file in OUTDIR (defaults to cwd)."""
|
|
468
|
+
|
|
469
|
+
concrete_prefix = reader._now().strftime(prefix)
|
|
470
|
+
export_path = reader._storage.export(outdir, concrete_prefix)
|
|
471
|
+
click.echo(export_path)
|
|
472
|
+
|
|
473
|
+
|
|
462
474
|
try:
|
|
463
475
|
import reader._app.cli
|
|
464
476
|
except ImportError:
|
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import pathlib
|
|
5
6
|
import sqlite3
|
|
6
7
|
import sys
|
|
7
8
|
from collections.abc import Callable
|
|
8
9
|
from collections.abc import Iterable
|
|
9
10
|
from functools import partial
|
|
10
11
|
from typing import Any
|
|
12
|
+
from typing import Self
|
|
11
13
|
from typing import TypeVar
|
|
12
14
|
|
|
13
15
|
from ..exceptions import StorageError
|
|
@@ -82,8 +84,9 @@ class StorageBase:
|
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
@wrap_exceptions()
|
|
85
|
-
def __enter__(self) ->
|
|
87
|
+
def __enter__(self) -> Self:
|
|
86
88
|
self.factory.__enter__()
|
|
89
|
+
return self
|
|
87
90
|
|
|
88
91
|
@wrap_exceptions()
|
|
89
92
|
def __exit__(self, *_: Any) -> None:
|
|
@@ -109,3 +112,30 @@ class StorageBase:
|
|
|
109
112
|
last,
|
|
110
113
|
row_factory,
|
|
111
114
|
)
|
|
115
|
+
|
|
116
|
+
def export(self, outdir: str | os.PathLike[str], prefix: str) -> pathlib.Path:
|
|
117
|
+
import gzip
|
|
118
|
+
import shutil
|
|
119
|
+
import tempfile
|
|
120
|
+
|
|
121
|
+
outdir = pathlib.Path(outdir)
|
|
122
|
+
gz = outdir / self.export_name(prefix)
|
|
123
|
+
|
|
124
|
+
with tempfile.TemporaryDirectory(f".{prefix}", dir=outdir) as tmpdir_str:
|
|
125
|
+
tmpdir = pathlib.Path(tmpdir_str)
|
|
126
|
+
|
|
127
|
+
tmp_db = tmpdir / 'db'
|
|
128
|
+
self.get_db().execute("vacuum into ?", (str(tmp_db),))
|
|
129
|
+
|
|
130
|
+
# the file name is stored in the .gz file
|
|
131
|
+
tmp_gz = tmpdir / gz.with_suffix('').name
|
|
132
|
+
with tmp_db.open('rb') as db_file:
|
|
133
|
+
with gzip.open(tmp_gz, 'wb') as gz_file:
|
|
134
|
+
shutil.copyfileobj(db_file, gz_file)
|
|
135
|
+
|
|
136
|
+
tmp_gz.replace(gz)
|
|
137
|
+
|
|
138
|
+
return gz
|
|
139
|
+
|
|
140
|
+
def export_name(self, prefix: str) -> str:
|
|
141
|
+
return f"{prefix}.sqlite.gz"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
3
5
|
from collections.abc import Iterable
|
|
4
6
|
from collections.abc import Mapping
|
|
5
7
|
from collections.abc import Sequence
|
|
@@ -727,7 +729,7 @@ class StorageType(Protocol): # pragma: no cover
|
|
|
727
729
|
|
|
728
730
|
"""
|
|
729
731
|
|
|
730
|
-
def __enter__(self) ->
|
|
732
|
+
def __enter__(self) -> Self:
|
|
731
733
|
"""Called when :class:`.Reader` is used as a context manager."""
|
|
732
734
|
|
|
733
735
|
def __exit__(self, *_: Any) -> None:
|
|
@@ -1113,6 +1115,17 @@ class StorageType(Protocol): # pragma: no cover
|
|
|
1113
1115
|
|
|
1114
1116
|
"""
|
|
1115
1117
|
|
|
1118
|
+
def export(self, outdir: str | os.PathLike[str], prefix: str) -> pathlib.Path:
|
|
1119
|
+
"""Export all data to a file in ``outdir`` and return its full path.
|
|
1120
|
+
|
|
1121
|
+
Aside from starting with ``prefix``,
|
|
1122
|
+
the file name and contents are storage-dependent.
|
|
1123
|
+
|
|
1124
|
+
"""
|
|
1125
|
+
|
|
1126
|
+
def export_name(self, prefix: str) -> str:
|
|
1127
|
+
"""Return the file name of an export."""
|
|
1128
|
+
|
|
1116
1129
|
|
|
1117
1130
|
@runtime_checkable
|
|
1118
1131
|
class BoundSearchStorageType(StorageType, Protocol):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.26
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -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"
|
|
@@ -20,11 +20,13 @@ docs/internal.rst
|
|
|
20
20
|
docs/plugins.rst
|
|
21
21
|
docs/tutorial.rst
|
|
22
22
|
docs/why.rst
|
|
23
|
+
docs/screenshots/autodiscovery.png
|
|
23
24
|
docs/screenshots/empty.png
|
|
24
25
|
docs/screenshots/entries-filters-light.png
|
|
25
26
|
docs/screenshots/entries.png
|
|
26
27
|
docs/screenshots/entry-image-light.png
|
|
27
28
|
docs/screenshots/entry.png
|
|
29
|
+
docs/screenshots/export-database.png
|
|
28
30
|
docs/screenshots/feed.png
|
|
29
31
|
docs/screenshots/feeds.png
|
|
30
32
|
docs/screenshots/help.png
|
|
@@ -36,7 +38,6 @@ examples/feed_slugs.py
|
|
|
36
38
|
examples/parser_only.py
|
|
37
39
|
examples/podcast.py
|
|
38
40
|
examples/terminal.py
|
|
39
|
-
scripts/backup.sh
|
|
40
41
|
scripts/bench.py
|
|
41
42
|
scripts/debug_storage_stats.py
|
|
42
43
|
scripts/entry_dedupe_backtest.py
|
|
@@ -67,6 +68,7 @@ src/reader.egg-info/requires.txt
|
|
|
67
68
|
src/reader.egg-info/top_level.txt
|
|
68
69
|
src/reader/_app/__init__.py
|
|
69
70
|
src/reader/_app/cli.py
|
|
71
|
+
src/reader/_app/exports.py
|
|
70
72
|
src/reader/_app/ext.py
|
|
71
73
|
src/reader/_app/forms.py
|
|
72
74
|
src/reader/_app/wsgi.py
|
|
@@ -104,6 +106,8 @@ src/reader/_app/templates/import_feeds.html
|
|
|
104
106
|
src/reader/_app/templates/layout.html
|
|
105
107
|
src/reader/_app/templates/layout_base.html
|
|
106
108
|
src/reader/_app/templates/macros.html
|
|
109
|
+
src/reader/_app/templates/settings.html
|
|
110
|
+
src/reader/_app/templates/settings_base.html
|
|
107
111
|
src/reader/_parser/__init__.py
|
|
108
112
|
src/reader/_parser/_http_utils.py
|
|
109
113
|
src/reader/_parser/_url_utils.py
|