reader 3.15__tar.gz → 3.16__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.15 → reader-3.16}/CHANGES.rst +54 -0
- {reader-3.15/src/reader.egg-info → reader-3.16}/PKG-INFO +1 -1
- {reader-3.15 → reader-3.16}/docs/api.rst +10 -0
- {reader-3.15 → reader-3.16}/docs/dev.rst +17 -5
- {reader-3.15 → reader-3.16}/src/reader/__init__.py +2 -1
- {reader-3.15 → reader-3.16}/src/reader/_app/__init__.py +15 -19
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/entries.html +4 -1
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/entry.html +1 -1
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/macros.html +2 -8
- {reader-3.15 → reader-3.16}/src/reader/_parser/feedparser.py +20 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_base.py +6 -10
- {reader-3.15 → reader-3.16}/src/reader/_storage/_entries.py +34 -3
- {reader-3.15 → reader-3.16}/src/reader/_storage/_schema.py +8 -1
- {reader-3.15 → reader-3.16}/src/reader/_storage/_search.py +38 -13
- {reader-3.15 → reader-3.16}/src/reader/_storage/_sqlite_utils.py +10 -1
- {reader-3.15 → reader-3.16}/src/reader/_types.py +58 -0
- {reader-3.15 → reader-3.16}/src/reader/_update.py +1 -0
- {reader-3.15 → reader-3.16}/src/reader/_utils.py +7 -0
- {reader-3.15 → reader-3.16}/src/reader/core.py +106 -12
- {reader-3.15 → reader-3.16}/src/reader/types.py +68 -1
- reader-3.16/src/reader/utils.py +78 -0
- {reader-3.15 → reader-3.16/src/reader.egg-info}/PKG-INFO +1 -1
- {reader-3.15 → reader-3.16}/src/reader.egg-info/SOURCES.txt +3 -1
- {reader-3.15 → reader-3.16}/tests/data/full.atom +38 -0
- {reader-3.15 → reader-3.16}/tests/data/full.atom.py +31 -0
- {reader-3.15 → reader-3.16}/tests/data/full.rss +14 -0
- {reader-3.15 → reader-3.16}/tests/data/full.rss.py +16 -0
- {reader-3.15 → reader-3.16}/tests/data/relative.rss +1 -1
- {reader-3.15 → reader-3.16}/tests/data/relative.rss.py +1 -1
- {reader-3.15 → reader-3.16}/tests/test__types.py +5 -2
- {reader-3.15 → reader-3.16}/tests/test_cli.py +9 -8
- {reader-3.15 → reader-3.16}/tests/test_reader.py +118 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_filter.py +5 -1
- {reader-3.15 → reader-3.16}/tests/test_reader_integration.py +5 -5
- {reader-3.15 → reader-3.16}/tests/test_reader_search.py +90 -0
- reader-3.16/tests/test_reader_utils.py +59 -0
- {reader-3.15 → reader-3.16}/tests/test_storage.py +1 -6
- {reader-3.15 → reader-3.16}/LICENSE +0 -0
- {reader-3.15 → reader-3.16}/MANIFEST.in +0 -0
- {reader-3.15 → reader-3.16}/README.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/Makefile +0 -0
- {reader-3.15 → reader-3.16}/docs/app.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/changelog.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/cli.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/conf.py +0 -0
- {reader-3.15 → reader-3.16}/docs/config.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/contributing.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/dev-app.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/guide.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/images/redesign-01.png +0 -0
- {reader-3.15 → reader-3.16}/docs/index.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/install.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/internal.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/make.bat +0 -0
- {reader-3.15 → reader-3.16}/docs/plugins.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/dillo.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/entries-feed.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/entries.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/entry-one.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/entry-two.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/feeds.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/lynx.png +0 -0
- {reader-3.15 → reader-3.16}/docs/screenshots/search.png +0 -0
- {reader-3.15 → reader-3.16}/docs/tutorial.rst +0 -0
- {reader-3.15 → reader-3.16}/docs/why.rst +0 -0
- {reader-3.15 → reader-3.16}/examples/config.yaml +0 -0
- {reader-3.15 → reader-3.16}/examples/custom_headers.py +0 -0
- {reader-3.15 → reader-3.16}/examples/feed_slugs.py +0 -0
- {reader-3.15 → reader-3.16}/examples/parser_only.py +0 -0
- {reader-3.15 → reader-3.16}/examples/podcast.py +0 -0
- {reader-3.15 → reader-3.16}/examples/terminal.py +0 -0
- {reader-3.15 → reader-3.16}/pyproject.toml +0 -0
- {reader-3.15 → reader-3.16}/run.sh +0 -0
- {reader-3.15 → reader-3.16}/scripts/.coverage +0 -0
- {reader-3.15 → reader-3.16}/scripts/backup.sh +0 -0
- {reader-3.15 → reader-3.16}/scripts/bench.py +0 -0
- {reader-3.15 → reader-3.16}/scripts/debug_storage_stats.py +0 -0
- {reader-3.15 → reader-3.16}/scripts/generate_import_all.py +0 -0
- {reader-3.15 → reader-3.16}/scripts/jscontrols.html +0 -0
- {reader-3.15 → reader-3.16}/scripts/jscontrols.py +0 -0
- {reader-3.15 → reader-3.16}/scripts/lines.sh +0 -0
- {reader-3.15 → reader-3.16}/scripts/release.py +0 -0
- {reader-3.15 → reader-3.16}/setup.cfg +0 -0
- {reader-3.15 → reader-3.16}/src/reader/__main__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/api_thing.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/cli.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/static/controls.js +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/static/style.css +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/add_entry.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/feeds.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/metadata.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/templates/tags.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_app/wsgi.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_cli.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_config.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_hash_utils.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/_http_utils.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/_lazy.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/file.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/http.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/jsonfeed.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/requests/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_parser/requests/_lazy.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/enclosure_tags.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/preview_feed_list.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/share.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/sqlite_releases.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_plugins/timer.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_changes.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_feeds.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_html_utils.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_sql_utils.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_storage/_tags.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/exceptions.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/entry_dedupe.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/mark_as_read.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/readtime.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/plugins/ua_fallback.py +0 -0
- {reader-3.15 → reader-3.16}/src/reader/py.typed +0 -0
- {reader-3.15 → reader-3.16}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.15 → reader-3.16}/src/reader.egg-info/requires.txt +0 -0
- {reader-3.15 → reader-3.16}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.15 → reader-3.16}/tests/conftest.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/10.json +0 -0
- {reader-3.15 → reader-3.16}/tests/data/10.json.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/custom +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.atom +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.atom.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.json +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.json.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.rss +0 -0
- {reader-3.15 → reader-3.16}/tests/data/empty.rss.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/full.json +0 -0
- {reader-3.15 → reader-3.16}/tests/data/full.json.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/invalid.json +0 -0
- {reader-3.15 → reader-3.16}/tests/data/invalid.json.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/relative.atom +0 -0
- {reader-3.15 → reader-3.16}/tests/data/relative.atom.py +0 -0
- {reader-3.15 → reader-3.16}/tests/data/sqlite_releases.html +0 -0
- {reader-3.15 → reader-3.16}/tests/data/unknown.json +0 -0
- {reader-3.15 → reader-3.16}/tests/data/unknown.json.py +0 -0
- {reader-3.15 → reader-3.16}/tests/fakeparser.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_methods.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_test_plugins/__init__.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_test_plugins/good.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_test_plugins/init_error.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_test_plugins/missing_dependency.py +0 -0
- {reader-3.15 → reader-3.16}/tests/reader_test_plugins/missing_entry_point.py +0 -0
- /reader-3.15/tests/test_utils.py → /reader-3.16/tests/test__utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_app.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_app_wsgi.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_bench.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_changes.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_config.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_exceptions.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_hash_utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_html_utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_lazy_imports.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_parser.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_entry_dedupe.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_mark_as_read.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_preview_feed_list.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_readtime.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_context.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_counts.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_deprecations.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_hooks.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_plugins.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_private.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_sort.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_reader_update.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_search.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_sql_utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_sqlite_utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_tags.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_test_utils.py +0 -0
- {reader-3.15 → reader-3.16}/tests/test_types.py +0 -0
- {reader-3.15 → reader-3.16}/tests/utils.py +0 -0
- {reader-3.15 → reader-3.16}/tox.ini +0 -0
|
@@ -6,6 +6,60 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.16
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2024-12-08
|
|
13
|
+
|
|
14
|
+
* Parse and store :attr:`Entry.source` for Atom and RSS feeds. (:issue:`267`)
|
|
15
|
+
|
|
16
|
+
* Add :attr:`Entry.feed_resolved_title` and :attr:`Feed.resolved_title` properties.
|
|
17
|
+
* The ``feed`` search column now indexes :attr:`Entry.feed_resolved_title`,
|
|
18
|
+
instead of feed :attr:`~Feed.user_title` or :attr:`~Feed.title`.
|
|
19
|
+
* Allow filtering entries by the entry source.
|
|
20
|
+
|
|
21
|
+
* Add :func:`reader.utils.archive_entries` for
|
|
22
|
+
copying entries to an "archived" feed.
|
|
23
|
+
(:issue:`290`)
|
|
24
|
+
|
|
25
|
+
* Add :meth:`~Reader.copy_entry`.
|
|
26
|
+
* Allow archiving entries from the web app.
|
|
27
|
+
|
|
28
|
+
* Fix bug causing :class:`Reader` operations
|
|
29
|
+
from a thread other than the one that created the instance
|
|
30
|
+
to happen with foreign key constraint enforcement disabled
|
|
31
|
+
(e.g. deleting a feed from another thread would not delete its entries).
|
|
32
|
+
|
|
33
|
+
This bug exists since using :class:`Reader` instances from other threads
|
|
34
|
+
became allowed in `2.15 <Version 2.15_>`_.
|
|
35
|
+
|
|
36
|
+
Serving the web application with ``python -m reader serve``
|
|
37
|
+
is known to be affected.
|
|
38
|
+
Serving it with uWSGI without threads (the default)
|
|
39
|
+
should not be affected.
|
|
40
|
+
|
|
41
|
+
.. attention::
|
|
42
|
+
|
|
43
|
+
**Your database may be in an inconsistent state because of this bug.**
|
|
44
|
+
|
|
45
|
+
It is recommended you run `PRAGMA foreign_key_check`_ on your database.
|
|
46
|
+
|
|
47
|
+
If you are upgrading from a version prior to 3.16
|
|
48
|
+
(i.e. were not using a pre-release version of *reader*),
|
|
49
|
+
the migration will do so for you.
|
|
50
|
+
If there are inconsistencies, you will get this error::
|
|
51
|
+
|
|
52
|
+
StorageError: integrity error: after migrating to version 43:
|
|
53
|
+
integrity error: FOREIGN KEY constraint failed
|
|
54
|
+
|
|
55
|
+
* Fix :meth:`~Reader.enable_search` / :meth:`~Reader.update_search`
|
|
56
|
+
not working when the search database is missing but change tracking is enabled
|
|
57
|
+
(e.g. when restoring the main database from backup).
|
|
58
|
+
(:issue:`362`)
|
|
59
|
+
|
|
60
|
+
.. _PRAGMA foreign_key_check: https://www.sqlite.org/pragma.html#pragma_foreign_key_check
|
|
61
|
+
|
|
62
|
+
|
|
9
63
|
Version 3.15
|
|
10
64
|
------------
|
|
11
65
|
|
|
@@ -40,6 +40,9 @@ Data objects
|
|
|
40
40
|
.. autoclass:: Enclosure
|
|
41
41
|
:members:
|
|
42
42
|
|
|
43
|
+
.. autoclass:: EntrySource
|
|
44
|
+
:members:
|
|
45
|
+
|
|
43
46
|
.. autoclass:: EntrySearchResult
|
|
44
47
|
:members:
|
|
45
48
|
|
|
@@ -187,3 +190,10 @@ Constants
|
|
|
187
190
|
.. autodata:: reader.core.DEFAULT_RESERVED_NAME_SCHEME
|
|
188
191
|
|
|
189
192
|
.. autodata:: reader.plugins.DEFAULT_PLUGINS
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
Utilities
|
|
197
|
+
---------
|
|
198
|
+
|
|
199
|
+
.. autofunction:: reader.utils.archive_entries
|
|
@@ -34,6 +34,9 @@ but I will prioritize supporting :doc:`contributors <contributing>`
|
|
|
34
34
|
|
|
35
35
|
* OPML support, :issue:`165`
|
|
36
36
|
* :ref:`deleting entries <deleting entries>`
|
|
37
|
+
|
|
38
|
+
* archiving important entries of deleted feeds, :issue:`290`
|
|
39
|
+
|
|
37
40
|
* :ref:`feed interaction statistics <counts api>`
|
|
38
41
|
|
|
39
42
|
* security
|
|
@@ -69,7 +72,6 @@ but I will prioritize supporting :doc:`contributors <contributing>`
|
|
|
69
72
|
|
|
70
73
|
* add more fields to data objects
|
|
71
74
|
|
|
72
|
-
* entry source, :issue:`276`
|
|
73
75
|
* extra data, as an escape hatch, :issue:`277`
|
|
74
76
|
|
|
75
77
|
* :ref:`multiple storage implementations <multiple storage implementations>`
|
|
@@ -512,6 +514,16 @@ From the initial issue:
|
|
|
512
514
|
* use cases: :issue:`149#issuecomment-700066794`
|
|
513
515
|
* initial requirements: :issue:`149#issuecomment-700532183`
|
|
514
516
|
|
|
517
|
+
Splitting a feed into two feeds is discussed in :issue:`221`.
|
|
518
|
+
The main takeaway at the time (2021) was that
|
|
519
|
+
not all entry data can be retrieved through the storage API,
|
|
520
|
+
so it's not fully possible to round-trip an entry.
|
|
521
|
+
Based on a quick 2024 survey,
|
|
522
|
+
the only affected attributes
|
|
523
|
+
are :attr:`~.EntryUpdateIntent.feed_order` (can't get; not critical)
|
|
524
|
+
and :attr:`~Entry.original_feed_url` (can't set; will be required for
|
|
525
|
+
archiving important entries of deleted feeds, :issue:`290`).
|
|
526
|
+
|
|
515
527
|
|
|
516
528
|
Resource tags / metadata
|
|
517
529
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
@@ -708,16 +720,16 @@ Wrapping underlying storage exceptions
|
|
|
708
720
|
|
|
709
721
|
Which exception to wrap, and which not: :issue:`21#issuecomment-365442439`.
|
|
710
722
|
|
|
711
|
-
In version 3.10 (November 2023), all internal APIs were changed
|
|
712
|
-
to use timezone-aware datetimes, with the timezone set to UTC,
|
|
713
|
-
in preparation for support for any timezone.
|
|
714
|
-
|
|
715
723
|
|
|
716
724
|
Timezone handling
|
|
717
725
|
~~~~~~~~~~~~~~~~~
|
|
718
726
|
|
|
719
727
|
Aware vs. naive, and what's needed to go fully aware: :issue:`233#issuecomment-881618002`.
|
|
720
728
|
|
|
729
|
+
In version 3.10 (November 2023), all internal APIs were changed
|
|
730
|
+
to use timezone-aware datetimes, with the timezone set to UTC,
|
|
731
|
+
in preparation for support for any timezone.
|
|
732
|
+
|
|
721
733
|
|
|
722
734
|
OPML support
|
|
723
735
|
~~~~~~~~~~~~
|
|
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
|
|
|
37
37
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
__version__ = '3.
|
|
40
|
+
__version__ = '3.16'
|
|
41
41
|
|
|
42
42
|
# isort: off
|
|
43
43
|
|
|
@@ -52,6 +52,7 @@ from .types import (
|
|
|
52
52
|
Entry as Entry,
|
|
53
53
|
Content as Content,
|
|
54
54
|
Enclosure as Enclosure,
|
|
55
|
+
EntrySource as EntrySource,
|
|
55
56
|
EntrySearchResult as EntrySearchResult,
|
|
56
57
|
HighlightedString as HighlightedString,
|
|
57
58
|
FeedCounts as FeedCounts,
|
|
@@ -38,6 +38,7 @@ from reader import ReaderError
|
|
|
38
38
|
from reader._plugins import Loader
|
|
39
39
|
from reader.types import _get_entry_content
|
|
40
40
|
from reader.types import TristateFilterInput
|
|
41
|
+
from reader.utils import archive_entries
|
|
41
42
|
|
|
42
43
|
from .api_thing import APIError
|
|
43
44
|
from .api_thing import APIThing
|
|
@@ -157,12 +158,8 @@ class EntryProxy:
|
|
|
157
158
|
def title(self):
|
|
158
159
|
highlight = self._search_result.metadata.get('.title')
|
|
159
160
|
if highlight:
|
|
160
|
-
return
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
@property
|
|
164
|
-
def feed(self):
|
|
165
|
-
return FeedProxy(self._search_result, self._entry)
|
|
161
|
+
return highlighted(highlight)
|
|
162
|
+
return self._entry.title
|
|
166
163
|
|
|
167
164
|
@property
|
|
168
165
|
def summary(self):
|
|
@@ -184,21 +181,12 @@ class EntryProxy:
|
|
|
184
181
|
def get_content(self, prefer_summary=False):
|
|
185
182
|
return _get_entry_content(self, prefer_summary)
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
@dataclass(frozen=True)
|
|
189
|
-
class FeedProxy:
|
|
190
|
-
_search_result: EntrySearchResult
|
|
191
|
-
_entry: Entry
|
|
192
|
-
|
|
193
|
-
def __getattr__(self, name):
|
|
194
|
-
return getattr(self._entry.feed, name)
|
|
195
|
-
|
|
196
184
|
@property
|
|
197
|
-
def
|
|
198
|
-
highlight = self._search_result.metadata.get('.
|
|
185
|
+
def feed_resolved_title(self):
|
|
186
|
+
highlight = self._search_result.metadata.get('.feed_resolved_title')
|
|
199
187
|
if highlight:
|
|
200
|
-
return
|
|
201
|
-
return self._entry.
|
|
188
|
+
return highlighted(highlight)
|
|
189
|
+
return self._entry.feed_resolved_url
|
|
202
190
|
|
|
203
191
|
|
|
204
192
|
@dataclass
|
|
@@ -643,6 +631,14 @@ def mark_all_as_unread(data):
|
|
|
643
631
|
get_reader().mark_entry_as_unread((feed_url, entry_id))
|
|
644
632
|
|
|
645
633
|
|
|
634
|
+
@form_api(really=True)
|
|
635
|
+
@readererror_to_apierror()
|
|
636
|
+
def archive_all(data):
|
|
637
|
+
feed_url = data['feed-url']
|
|
638
|
+
entry_ids = json.loads(data['entry-id'])
|
|
639
|
+
archive_entries(get_reader(), [(feed_url, eid) for eid in entry_ids])
|
|
640
|
+
|
|
641
|
+
|
|
646
642
|
@form_api
|
|
647
643
|
@readererror_to_apierror()
|
|
648
644
|
def mark_as_important(data):
|
|
@@ -172,6 +172,9 @@
|
|
|
172
172
|
{{- macros.confirm_button('.form_api', 'mark-all-as-unread', 'all unread', leave_disabled=true, next=next, context={'feed-url': feed.url, 'entry-id': entries_data | tojson}) -}}
|
|
173
173
|
{% endif %}
|
|
174
174
|
|
|
175
|
+
{{- macros.confirm_button('.form_api', 'archive-all', 'archive all', leave_disabled=true, next=url_for('.entries', feed='reader:archived'), context={'feed-url': feed.url, 'entry-id': entries_data | tojson}) -}}
|
|
176
|
+
{# TODO: should redirect to archived # entry #}
|
|
177
|
+
|
|
175
178
|
{{ macros.confirm_button('.form_api', 'delete-feed', 'delete feed', leave_disabled=true, next=url_for('.entries'), context={'feed-url': feed.url}) }}
|
|
176
179
|
|
|
177
180
|
{{ macros.text_input_button('.form_api', 'update-feed-title', 'update feed title', 'feed-title', 'feed title', leave_disabled=true, next=next, context={'feed-url': feed.url}, value=feed.user_title) }}
|
|
@@ -265,7 +268,7 @@
|
|
|
265
268
|
<ul class="controls">
|
|
266
269
|
|
|
267
270
|
<li>
|
|
268
|
-
<a href="{{ url_for('.entries', feed=entry.feed.url) }}">{{
|
|
271
|
+
<a href="{{ url_for('.entries', feed=entry.feed.url) }}">{{ entry.feed_resolved_title or feed.url }}</a>
|
|
269
272
|
<li>
|
|
270
273
|
{%- set published = entry.published or entry.updated_not_none -%}
|
|
271
274
|
<span title="{{ published }}">{{ published | humanize_naturaltime }}</span>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<li>
|
|
21
21
|
{% if entry.author %} by {{ entry.author }}{% endif %}
|
|
22
|
-
in <a href="{{ url_for('.entries', feed=entry.feed.url) }}">{{
|
|
22
|
+
in <a href="{{ url_for('.entries', feed=entry.feed.url) }}">{{ entry.feed_resolved_title or feed.url }}</a>
|
|
23
23
|
<li>
|
|
24
24
|
{%- set published = entry.published or entry.updated_not_none -%}
|
|
25
25
|
<span title="{{ published }}">{{ published | humanize_naturaltime }}</span>
|
|
@@ -6,17 +6,11 @@
|
|
|
6
6
|
{%- endmacro %}
|
|
7
7
|
|
|
8
8
|
{% macro feed_title(feed) %}
|
|
9
|
-
{
|
|
10
|
-
{{ feed.user_title }}
|
|
11
|
-
{%- elif feed.title and feed.title.strip() -%}
|
|
12
|
-
{{ feed.title }}
|
|
13
|
-
{%- else -%}
|
|
14
|
-
{{ feed.url }}
|
|
15
|
-
{%- endif -%}
|
|
9
|
+
{{- feed.resolved_title or feed.url -}}
|
|
16
10
|
{% endmacro %}
|
|
17
11
|
|
|
18
12
|
{% macro feed_title_secondary(feed) %}
|
|
19
|
-
{%- if feed.
|
|
13
|
+
{%- if feed.title and feed.resolved_title != feed.title -%}
|
|
20
14
|
{{ feed.title }}
|
|
21
15
|
{%- endif -%}
|
|
22
16
|
{% endmacro %}
|
|
@@ -16,6 +16,7 @@ from .._types import FeedData
|
|
|
16
16
|
from ..exceptions import ParseError
|
|
17
17
|
from ..types import Content
|
|
18
18
|
from ..types import Enclosure
|
|
19
|
+
from ..types import EntrySource
|
|
19
20
|
from ._http_utils import parse_accept_header
|
|
20
21
|
from ._http_utils import unparse_accept_header
|
|
21
22
|
|
|
@@ -166,6 +167,24 @@ def _process_entry(feed_url: str, entry: Any, is_rss: bool) -> EntryData:
|
|
|
166
167
|
del data['length']
|
|
167
168
|
enclosures.append(Enclosure(**data))
|
|
168
169
|
|
|
170
|
+
source = None
|
|
171
|
+
if data := entry.get('source'):
|
|
172
|
+
links_by_rel: dict[str, list[Any]] = {}
|
|
173
|
+
for link in data.get('links', ()):
|
|
174
|
+
links_by_rel.setdefault(link.get('rel'), []).append(link)
|
|
175
|
+
source_url = data.get('url') or links_by_rel.get('self', [{}])[0].get('href')
|
|
176
|
+
source_title = data.get('title')
|
|
177
|
+
# choice of the set of required attributes is somewhat arbitrary...
|
|
178
|
+
if source_url or source_title:
|
|
179
|
+
source = EntrySource(
|
|
180
|
+
source_url,
|
|
181
|
+
_get_datetime_attr(data, 'updated_parsed'),
|
|
182
|
+
source_title,
|
|
183
|
+
data.get('link'),
|
|
184
|
+
data.get('author'),
|
|
185
|
+
data.get('subtitle'),
|
|
186
|
+
)
|
|
187
|
+
|
|
169
188
|
return EntryData(
|
|
170
189
|
feed_url,
|
|
171
190
|
id,
|
|
@@ -177,4 +196,5 @@ def _process_entry(feed_url: str, entry: Any, is_rss: bool) -> EntryData:
|
|
|
177
196
|
entry.get('summary'),
|
|
178
197
|
tuple(content),
|
|
179
198
|
tuple(enclosures),
|
|
199
|
+
source,
|
|
180
200
|
)
|
|
@@ -57,16 +57,12 @@ class StorageBase:
|
|
|
57
57
|
if timeout is not None:
|
|
58
58
|
kwargs['timeout'] = timeout
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raise
|
|
67
|
-
|
|
68
|
-
self.path = path
|
|
69
|
-
self.timeout = timeout
|
|
60
|
+
# at least the "PRAGMA foreign_keys = ON" part of setup_db
|
|
61
|
+
# has to run for every connection (in every thread),
|
|
62
|
+
# since it's not persisted across connections
|
|
63
|
+
self.factory = _sqlite_utils.LocalConnectionFactory(
|
|
64
|
+
path, self.setup_db, **kwargs
|
|
65
|
+
)
|
|
70
66
|
|
|
71
67
|
def get_db(self) -> sqlite3.Connection:
|
|
72
68
|
return self.factory()
|
|
@@ -26,6 +26,7 @@ from ..types import Enclosure
|
|
|
26
26
|
from ..types import Entry
|
|
27
27
|
from ..types import EntryCounts
|
|
28
28
|
from ..types import EntrySort
|
|
29
|
+
from ..types import EntrySource
|
|
29
30
|
from ._base import wrap_exceptions
|
|
30
31
|
from ._feeds import feed_factory
|
|
31
32
|
from ._sql_utils import Query
|
|
@@ -251,12 +252,14 @@ class EntriesMixin(StorageBase):
|
|
|
251
252
|
summary,
|
|
252
253
|
content,
|
|
253
254
|
enclosures,
|
|
255
|
+
source,
|
|
254
256
|
read,
|
|
255
257
|
last_updated,
|
|
256
258
|
first_updated,
|
|
257
259
|
first_updated_epoch,
|
|
258
260
|
feed_order,
|
|
259
261
|
recent_sort,
|
|
262
|
+
original_feed,
|
|
260
263
|
data_hash,
|
|
261
264
|
data_hash_changed,
|
|
262
265
|
added_by
|
|
@@ -271,12 +274,14 @@ class EntriesMixin(StorageBase):
|
|
|
271
274
|
:summary,
|
|
272
275
|
:content,
|
|
273
276
|
:enclosures,
|
|
277
|
+
:source,
|
|
274
278
|
0, -- read (should be not null in the schema, but isn't)
|
|
275
279
|
:last_updated,
|
|
276
280
|
:first_updated,
|
|
277
281
|
:first_updated_epoch,
|
|
278
282
|
:feed_order,
|
|
279
283
|
:recent_sort,
|
|
284
|
+
:original_feed,
|
|
280
285
|
:data_hash,
|
|
281
286
|
:data_hash_changed,
|
|
282
287
|
:added_by
|
|
@@ -296,10 +301,11 @@ class EntriesMixin(StorageBase):
|
|
|
296
301
|
summary = :summary,
|
|
297
302
|
content = :content,
|
|
298
303
|
enclosures = :enclosures,
|
|
304
|
+
source = :source,
|
|
299
305
|
last_updated = :last_updated,
|
|
300
306
|
feed_order = :feed_order,
|
|
301
307
|
recent_sort = :recent_sort,
|
|
302
|
-
original_feed =
|
|
308
|
+
original_feed = :original_feed,
|
|
303
309
|
data_hash = :data_hash,
|
|
304
310
|
data_hash_changed = :data_hash_changed,
|
|
305
311
|
added_by = :added_by
|
|
@@ -425,6 +431,7 @@ def get_entries_query(
|
|
|
425
431
|
entries.summary
|
|
426
432
|
entries.content
|
|
427
433
|
entries.enclosures
|
|
434
|
+
entries.source
|
|
428
435
|
entries.read
|
|
429
436
|
entries.read_modified
|
|
430
437
|
entries.important
|
|
@@ -456,6 +463,7 @@ def entry_factory(row: tuple[Any, ...]) -> Entry:
|
|
|
456
463
|
summary,
|
|
457
464
|
content,
|
|
458
465
|
enclosures,
|
|
466
|
+
source,
|
|
459
467
|
read,
|
|
460
468
|
read_modified,
|
|
461
469
|
important,
|
|
@@ -465,7 +473,15 @@ def entry_factory(row: tuple[Any, ...]) -> Entry:
|
|
|
465
473
|
last_updated,
|
|
466
474
|
original_feed,
|
|
467
475
|
sequence,
|
|
468
|
-
) = row[14:
|
|
476
|
+
) = row[14:33]
|
|
477
|
+
|
|
478
|
+
source_obj = None
|
|
479
|
+
if source:
|
|
480
|
+
source_dict = json.loads(source)
|
|
481
|
+
if source_dict['updated']:
|
|
482
|
+
source_dict['updated'] = convert_timestamp(source_dict['updated'])
|
|
483
|
+
source_obj = EntrySource(**source_dict)
|
|
484
|
+
|
|
469
485
|
return Entry(
|
|
470
486
|
id,
|
|
471
487
|
convert_timestamp(updated) if updated else None,
|
|
@@ -476,6 +492,7 @@ def entry_factory(row: tuple[Any, ...]) -> Entry:
|
|
|
476
492
|
summary,
|
|
477
493
|
tuple(Content(**d) for d in json.loads(content)) if content else (),
|
|
478
494
|
tuple(Enclosure(**d) for d in json.loads(enclosures)) if enclosures else (),
|
|
495
|
+
source_obj,
|
|
479
496
|
read == 1,
|
|
480
497
|
convert_timestamp(read_modified) if read_modified else None,
|
|
481
498
|
important == 1 if important is not None else None,
|
|
@@ -503,7 +520,9 @@ def entry_filter(
|
|
|
503
520
|
query: Query, filter: EntryFilter, keyword: str = 'WHERE'
|
|
504
521
|
) -> dict[str, Any]:
|
|
505
522
|
add = getattr(query, keyword)
|
|
506
|
-
feed_url, entry_id, read, important, has_enclosures, tags, feed_tags =
|
|
523
|
+
feed_url, entry_id, read, important, has_enclosures, source_url, tags, feed_tags = (
|
|
524
|
+
filter
|
|
525
|
+
)
|
|
507
526
|
|
|
508
527
|
context = {}
|
|
509
528
|
|
|
@@ -529,6 +548,10 @@ def entry_filter(
|
|
|
529
548
|
"""
|
|
530
549
|
)
|
|
531
550
|
|
|
551
|
+
if source_url:
|
|
552
|
+
add("json_extract(entries.source, '$.url') = :source_url")
|
|
553
|
+
context['source_url'] = source_url
|
|
554
|
+
|
|
532
555
|
context.update(entry_tags_filter(query, tags, keyword=keyword))
|
|
533
556
|
context.update(feed_tags_filter(query, feed_tags, 'entries.feed', keyword=keyword))
|
|
534
557
|
|
|
@@ -660,7 +683,15 @@ def entry_update_intent_to_dict(intent: EntryUpdateIntent) -> dict[str, Any]:
|
|
|
660
683
|
else None
|
|
661
684
|
),
|
|
662
685
|
recent_sort=adapt_datetime(intent.recent_sort) if intent.recent_sort else None,
|
|
686
|
+
original_feed=intent.original_feed_url,
|
|
663
687
|
data_hash=entry.hash,
|
|
664
688
|
data_hash_changed=context.pop('hash_changed'),
|
|
665
689
|
)
|
|
690
|
+
|
|
691
|
+
if entry.source:
|
|
692
|
+
source_dict = entry.source._asdict()
|
|
693
|
+
if entry.source.updated:
|
|
694
|
+
source_dict['updated'] = adapt_datetime(entry.source.updated)
|
|
695
|
+
context['source'] = json.dumps(source_dict)
|
|
696
|
+
|
|
666
697
|
return context
|
|
@@ -48,6 +48,7 @@ CREATE TABLE entries (
|
|
|
48
48
|
summary TEXT,
|
|
49
49
|
content TEXT,
|
|
50
50
|
enclosures TEXT,
|
|
51
|
+
source TEXT,
|
|
51
52
|
original_feed TEXT, -- null if the feed was never moved
|
|
52
53
|
data_hash BLOB, -- derived from entry data
|
|
53
54
|
data_hash_changed INTEGER, -- metadata about data_hash
|
|
@@ -318,7 +319,12 @@ def update_from_41_to_42(db: sqlite3.Connection, /) -> None: # pragma: no cover
|
|
|
318
319
|
entry_tags_by_key_index.create(db)
|
|
319
320
|
|
|
320
321
|
|
|
321
|
-
|
|
322
|
+
def update_from_42_to_43(db: sqlite3.Connection, /) -> None: # pragma: no cover
|
|
323
|
+
# https://github.com/lemon24/reader/issues/276
|
|
324
|
+
db.execute("ALTER TABLE entries ADD COLUMN source TEXT;")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
VERSION = 43
|
|
322
328
|
|
|
323
329
|
MIGRATIONS = {
|
|
324
330
|
# 1-9 removed before 0.1 (last in e4769d8ba77c61ec1fe2fbe99839e1826c17ace7)
|
|
@@ -331,6 +337,7 @@ MIGRATIONS = {
|
|
|
331
337
|
39: update_from_39_to_40,
|
|
332
338
|
40: update_from_40_to_41,
|
|
333
339
|
41: update_from_41_to_42,
|
|
340
|
+
42: update_from_42_to_43,
|
|
334
341
|
}
|
|
335
342
|
MISSING_SUFFIX = (
|
|
336
343
|
"; you may have skipped some required migrations, see "
|
|
@@ -19,6 +19,7 @@ from .._types import Action
|
|
|
19
19
|
from .._types import Change
|
|
20
20
|
from .._types import EntryFilter
|
|
21
21
|
from .._utils import exactly_one
|
|
22
|
+
from .._utils import resolve_path
|
|
22
23
|
from .._utils import zero_or_one
|
|
23
24
|
from ..exceptions import ChangeTrackingNotEnabledError
|
|
24
25
|
from ..exceptions import EntryNotFoundError
|
|
@@ -64,6 +65,13 @@ QUERY_EXC = dict.fromkeys(
|
|
|
64
65
|
# 255 letters / 4.7 letters per word (average in English)
|
|
65
66
|
TOKENS = 54
|
|
66
67
|
|
|
68
|
+
FEED_TITLE_PATHS = [
|
|
69
|
+
'.feed.title',
|
|
70
|
+
'.feed.user_title',
|
|
71
|
+
'.source.title',
|
|
72
|
+
'.feed_resolved_title',
|
|
73
|
+
]
|
|
74
|
+
|
|
67
75
|
|
|
68
76
|
class Search:
|
|
69
77
|
"""Search provider tightly coupled to the SQLite storage.
|
|
@@ -108,15 +116,18 @@ class Search:
|
|
|
108
116
|
|
|
109
117
|
@wrap_exceptions()
|
|
110
118
|
def enable(self) -> None:
|
|
111
|
-
self.storage.changes.enable()
|
|
112
119
|
try:
|
|
113
120
|
with ddl_transaction(self.get_db()) as db:
|
|
114
121
|
self._enable(db, self.schema)
|
|
115
122
|
except sqlite3.OperationalError as e:
|
|
116
|
-
if "table entries_search already exists" in str(e)
|
|
117
|
-
return
|
|
118
|
-
else: # pragma: no cover
|
|
123
|
+
if "table entries_search already exists" not in str(e): # pragma: no cover
|
|
119
124
|
raise
|
|
125
|
+
else:
|
|
126
|
+
# if search was not already enabled, make sure changes.enable()
|
|
127
|
+
# is not a no-op and we backfill all the resources
|
|
128
|
+
# (changes can be enabled and search not when restoring from backup)
|
|
129
|
+
self.storage.changes.disable()
|
|
130
|
+
self.storage.changes.enable()
|
|
120
131
|
|
|
121
132
|
@classmethod
|
|
122
133
|
def _enable(cls, db: sqlite3.Connection, schema: str = 'main') -> None:
|
|
@@ -211,19 +222,22 @@ class Search:
|
|
|
211
222
|
def is_enabled(self) -> bool:
|
|
212
223
|
return self._is_enabled(self.get_db())
|
|
213
224
|
|
|
214
|
-
@
|
|
215
|
-
def _is_enabled(db: sqlite3.Connection) -> bool:
|
|
225
|
+
@classmethod
|
|
226
|
+
def _is_enabled(cls, db: sqlite3.Connection) -> bool:
|
|
216
227
|
# Private API, may be called from migrations.
|
|
217
228
|
try:
|
|
218
|
-
|
|
229
|
+
cls._enabled_check(db)
|
|
219
230
|
except sqlite3.OperationalError as e:
|
|
220
|
-
if "no such table: entries_search" in str(e):
|
|
221
|
-
return False
|
|
222
|
-
else: # pragma: no cover
|
|
231
|
+
if "no such table: entries_search" not in str(e): # pragma: no cover
|
|
223
232
|
raise
|
|
233
|
+
return False
|
|
224
234
|
else:
|
|
225
235
|
return True
|
|
226
236
|
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _enabled_check(db: sqlite3.Connection) -> None:
|
|
239
|
+
list(db.execute("SELECT * FROM entries_search LIMIT 0;"))
|
|
240
|
+
|
|
227
241
|
@wrap_exceptions(ENABLED_EXC)
|
|
228
242
|
def update(self) -> None:
|
|
229
243
|
try:
|
|
@@ -232,6 +246,10 @@ class Search:
|
|
|
232
246
|
except ChangeTrackingNotEnabledError as e:
|
|
233
247
|
raise SearchNotEnabledError() from e
|
|
234
248
|
|
|
249
|
+
# if changes is enabled but search is not (e.g. restoring from backup),
|
|
250
|
+
# make sure we get a SearchNotEnabledError even if there are no changes
|
|
251
|
+
self._enabled_check(self.get_db())
|
|
252
|
+
|
|
235
253
|
def _delete_from_search(self) -> None:
|
|
236
254
|
# The loop is done outside of the chunk logic to help testing.
|
|
237
255
|
while changes := self.storage.changes.get(Action.DELETE):
|
|
@@ -327,8 +345,15 @@ class Search:
|
|
|
327
345
|
final.append((None, None))
|
|
328
346
|
|
|
329
347
|
stripped_title = self.strip_html(entry.title)
|
|
330
|
-
|
|
331
|
-
|
|
348
|
+
|
|
349
|
+
feed_resolved_title = entry.feed_resolved_title
|
|
350
|
+
for is_feed_user_title, path in enumerate(FEED_TITLE_PATHS): # noqa: B007
|
|
351
|
+
if feed_title := resolve_path(entry, path):
|
|
352
|
+
if feed_title == feed_resolved_title:
|
|
353
|
+
break
|
|
354
|
+
else:
|
|
355
|
+
feed_title = None
|
|
356
|
+
is_feed_user_title = 0
|
|
332
357
|
stripped_feed_title = self.strip_html(feed_title)
|
|
333
358
|
|
|
334
359
|
stripped[change] = [
|
|
@@ -579,7 +604,7 @@ def entry_search_result_factory(
|
|
|
579
604
|
if title:
|
|
580
605
|
metadata['.title'] = extract(title)
|
|
581
606
|
if feed_title:
|
|
582
|
-
path =
|
|
607
|
+
path = FEED_TITLE_PATHS[is_feed_user_title]
|
|
583
608
|
metadata[path] = extract(feed_title)
|
|
584
609
|
|
|
585
610
|
rv_content = {c['path']: extract(c['value']) for c in content if c['path']}
|
|
@@ -426,8 +426,11 @@ class LocalConnectionFactory:
|
|
|
426
426
|
|
|
427
427
|
INLINE_OPTIMIZE_TIMEOUT = 0.1
|
|
428
428
|
|
|
429
|
-
def __init__(
|
|
429
|
+
def __init__(
|
|
430
|
+
self, path: str, setup_db: _DBFunction = lambda _: None, **kwargs: Any
|
|
431
|
+
):
|
|
430
432
|
self.path = path
|
|
433
|
+
self.setup_db = setup_db
|
|
431
434
|
self.kwargs = kwargs
|
|
432
435
|
if kwargs.get('uri'): # pragma: no cover
|
|
433
436
|
raise NotImplementedError("is_private() does not work for uri=True")
|
|
@@ -463,6 +466,12 @@ class LocalConnectionFactory:
|
|
|
463
466
|
assert db is not None, "for mypy"
|
|
464
467
|
self._local.call_count = 0
|
|
465
468
|
|
|
469
|
+
try:
|
|
470
|
+
self.setup_db(db)
|
|
471
|
+
except BaseException:
|
|
472
|
+
db.close()
|
|
473
|
+
raise
|
|
474
|
+
|
|
466
475
|
# http://threebean.org/blog/atexit-for-threads/
|
|
467
476
|
# works on cpython (finalizer runs in thread),
|
|
468
477
|
# but not on pypy (finalizer runs in main thread);
|