reader 3.15__tar.gz → 3.17__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.
Files changed (237) hide show
  1. {reader-3.15 → reader-3.17}/CHANGES.rst +84 -0
  2. {reader-3.15 → reader-3.17}/MANIFEST.in +1 -0
  3. {reader-3.15/src/reader.egg-info → reader-3.17}/PKG-INFO +4 -2
  4. reader-3.17/docs/.DS_Store +0 -0
  5. {reader-3.15 → reader-3.17}/docs/api.rst +10 -0
  6. {reader-3.15 → reader-3.17}/docs/app.rst +25 -0
  7. {reader-3.15 → reader-3.17}/docs/dev.rst +28 -6
  8. reader-3.17/docs/screenshots/entries-v2-dark.png +0 -0
  9. reader-3.17/docs/screenshots/entries-v2-filters-light.png +0 -0
  10. {reader-3.15 → reader-3.17}/pyproject.toml +3 -0
  11. {reader-3.15 → reader-3.17}/src/reader/__init__.py +2 -1
  12. {reader-3.15 → reader-3.17}/src/reader/_app/__init__.py +20 -19
  13. {reader-3.15 → reader-3.17}/src/reader/_app/templates/entries.html +4 -1
  14. {reader-3.15 → reader-3.17}/src/reader/_app/templates/entry.html +1 -1
  15. {reader-3.15 → reader-3.17}/src/reader/_app/templates/layout.html +1 -0
  16. {reader-3.15 → reader-3.17}/src/reader/_app/templates/macros.html +2 -8
  17. reader-3.17/src/reader/_app/v2/__init__.py +123 -0
  18. reader-3.17/src/reader/_app/v2/forms.py +159 -0
  19. reader-3.17/src/reader/_app/v2/static/style.css +27 -0
  20. reader-3.17/src/reader/_app/v2/static/theme.js +91 -0
  21. reader-3.17/src/reader/_app/v2/templates/v2/entries.html +232 -0
  22. reader-3.17/src/reader/_app/v2/templates/v2/layout.html +116 -0
  23. reader-3.17/src/reader/_app/v2/templates/v2/macros.html +47 -0
  24. {reader-3.15 → reader-3.17}/src/reader/_parser/feedparser.py +22 -2
  25. {reader-3.15 → reader-3.17}/src/reader/_plugins/cli_status.py +25 -14
  26. {reader-3.15 → reader-3.17}/src/reader/_storage/_base.py +6 -10
  27. {reader-3.15 → reader-3.17}/src/reader/_storage/_entries.py +34 -3
  28. {reader-3.15 → reader-3.17}/src/reader/_storage/_schema.py +8 -1
  29. {reader-3.15 → reader-3.17}/src/reader/_storage/_search.py +38 -13
  30. {reader-3.15 → reader-3.17}/src/reader/_storage/_sqlite_utils.py +13 -2
  31. {reader-3.15 → reader-3.17}/src/reader/_types.py +58 -0
  32. {reader-3.15 → reader-3.17}/src/reader/_update.py +1 -0
  33. {reader-3.15 → reader-3.17}/src/reader/_utils.py +7 -0
  34. {reader-3.15 → reader-3.17}/src/reader/core.py +106 -12
  35. {reader-3.15 → reader-3.17}/src/reader/types.py +75 -1
  36. reader-3.17/src/reader/utils.py +78 -0
  37. {reader-3.15 → reader-3.17/src/reader.egg-info}/PKG-INFO +4 -2
  38. {reader-3.15 → reader-3.17}/src/reader.egg-info/SOURCES.txt +13 -1
  39. {reader-3.15 → reader-3.17}/src/reader.egg-info/requires.txt +2 -0
  40. {reader-3.15 → reader-3.17}/tests/conftest.py +15 -1
  41. {reader-3.15 → reader-3.17}/tests/data/full.atom +38 -0
  42. {reader-3.15 → reader-3.17}/tests/data/full.atom.py +31 -0
  43. {reader-3.15 → reader-3.17}/tests/data/full.rss +14 -0
  44. {reader-3.15 → reader-3.17}/tests/data/full.rss.py +16 -0
  45. {reader-3.15 → reader-3.17}/tests/data/relative.rss +1 -1
  46. {reader-3.15 → reader-3.17}/tests/data/relative.rss.py +1 -1
  47. {reader-3.15 → reader-3.17}/tests/fakeparser.py +7 -31
  48. {reader-3.15 → reader-3.17}/tests/test__types.py +5 -2
  49. {reader-3.15 → reader-3.17}/tests/test_app.py +11 -29
  50. {reader-3.15 → reader-3.17}/tests/test_changes.py +15 -17
  51. {reader-3.15 → reader-3.17}/tests/test_cli.py +9 -8
  52. {reader-3.15 → reader-3.17}/tests/test_plugins_cli_status.py +36 -27
  53. {reader-3.15 → reader-3.17}/tests/test_plugins_enclosure_dedupe.py +1 -3
  54. {reader-3.15 → reader-3.17}/tests/test_plugins_entry_dedupe.py +14 -21
  55. {reader-3.15 → reader-3.17}/tests/test_plugins_mark_as_read.py +9 -21
  56. {reader-3.15 → reader-3.17}/tests/test_plugins_readtime.py +4 -13
  57. {reader-3.15 → reader-3.17}/tests/test_reader.py +194 -192
  58. {reader-3.15 → reader-3.17}/tests/test_reader_context.py +1 -3
  59. {reader-3.15 → reader-3.17}/tests/test_reader_filter.py +5 -1
  60. {reader-3.15 → reader-3.17}/tests/test_reader_hooks.py +3 -9
  61. {reader-3.15 → reader-3.17}/tests/test_reader_integration.py +5 -5
  62. {reader-3.15 → reader-3.17}/tests/test_reader_private.py +11 -13
  63. {reader-3.15 → reader-3.17}/tests/test_reader_search.py +104 -45
  64. {reader-3.15 → reader-3.17}/tests/test_reader_sort.py +8 -10
  65. {reader-3.15 → reader-3.17}/tests/test_reader_update.py +17 -25
  66. reader-3.17/tests/test_reader_utils.py +57 -0
  67. {reader-3.15 → reader-3.17}/tests/test_storage.py +7 -16
  68. {reader-3.15 → reader-3.17}/tests/test_tags.py +5 -10
  69. {reader-3.15 → reader-3.17}/tests/test_types.py +31 -0
  70. {reader-3.15 → reader-3.17}/tests/utils.py +34 -0
  71. {reader-3.15 → reader-3.17}/tox.ini +3 -3
  72. {reader-3.15 → reader-3.17}/LICENSE +0 -0
  73. {reader-3.15 → reader-3.17}/README.rst +0 -0
  74. {reader-3.15 → reader-3.17}/docs/Makefile +0 -0
  75. {reader-3.15 → reader-3.17}/docs/changelog.rst +0 -0
  76. {reader-3.15 → reader-3.17}/docs/cli.rst +0 -0
  77. {reader-3.15 → reader-3.17}/docs/conf.py +0 -0
  78. {reader-3.15 → reader-3.17}/docs/config.rst +0 -0
  79. {reader-3.15 → reader-3.17}/docs/contributing.rst +0 -0
  80. {reader-3.15 → reader-3.17}/docs/dev-app.rst +0 -0
  81. {reader-3.15 → reader-3.17}/docs/guide.rst +0 -0
  82. {reader-3.15 → reader-3.17}/docs/images/redesign-01.png +0 -0
  83. {reader-3.15 → reader-3.17}/docs/index.rst +0 -0
  84. {reader-3.15 → reader-3.17}/docs/install.rst +0 -0
  85. {reader-3.15 → reader-3.17}/docs/internal.rst +0 -0
  86. {reader-3.15 → reader-3.17}/docs/make.bat +0 -0
  87. {reader-3.15 → reader-3.17}/docs/plugins.rst +0 -0
  88. {reader-3.15 → reader-3.17}/docs/screenshots/dillo.png +0 -0
  89. {reader-3.15 → reader-3.17}/docs/screenshots/entries-feed.png +0 -0
  90. {reader-3.15 → reader-3.17}/docs/screenshots/entries.png +0 -0
  91. {reader-3.15 → reader-3.17}/docs/screenshots/entry-one.png +0 -0
  92. {reader-3.15 → reader-3.17}/docs/screenshots/entry-two.png +0 -0
  93. {reader-3.15 → reader-3.17}/docs/screenshots/feeds.png +0 -0
  94. {reader-3.15 → reader-3.17}/docs/screenshots/lynx.png +0 -0
  95. {reader-3.15 → reader-3.17}/docs/screenshots/search.png +0 -0
  96. {reader-3.15 → reader-3.17}/docs/tutorial.rst +0 -0
  97. {reader-3.15 → reader-3.17}/docs/why.rst +0 -0
  98. {reader-3.15 → reader-3.17}/examples/config.yaml +0 -0
  99. {reader-3.15 → reader-3.17}/examples/custom_headers.py +0 -0
  100. {reader-3.15 → reader-3.17}/examples/feed_slugs.py +0 -0
  101. {reader-3.15 → reader-3.17}/examples/parser_only.py +0 -0
  102. {reader-3.15 → reader-3.17}/examples/podcast.py +0 -0
  103. {reader-3.15 → reader-3.17}/examples/terminal.py +0 -0
  104. {reader-3.15 → reader-3.17}/run.sh +0 -0
  105. {reader-3.15 → reader-3.17}/scripts/.coverage +0 -0
  106. {reader-3.15 → reader-3.17}/scripts/backup.sh +0 -0
  107. {reader-3.15 → reader-3.17}/scripts/bench.py +0 -0
  108. {reader-3.15 → reader-3.17}/scripts/debug_storage_stats.py +0 -0
  109. {reader-3.15 → reader-3.17}/scripts/generate_import_all.py +0 -0
  110. {reader-3.15 → reader-3.17}/scripts/jscontrols.html +0 -0
  111. {reader-3.15 → reader-3.17}/scripts/jscontrols.py +0 -0
  112. {reader-3.15 → reader-3.17}/scripts/lines.sh +0 -0
  113. {reader-3.15 → reader-3.17}/scripts/release.py +0 -0
  114. {reader-3.15 → reader-3.17}/setup.cfg +0 -0
  115. {reader-3.15 → reader-3.17}/src/reader/__main__.py +0 -0
  116. {reader-3.15 → reader-3.17}/src/reader/_app/api_thing.py +0 -0
  117. {reader-3.15 → reader-3.17}/src/reader/_app/cli.py +0 -0
  118. {reader-3.15 → reader-3.17}/src/reader/_app/static/controls.js +0 -0
  119. {reader-3.15 → reader-3.17}/src/reader/_app/static/style.css +0 -0
  120. {reader-3.15 → reader-3.17}/src/reader/_app/templates/add_entry.html +0 -0
  121. {reader-3.15 → reader-3.17}/src/reader/_app/templates/feeds.html +0 -0
  122. {reader-3.15 → reader-3.17}/src/reader/_app/templates/metadata.html +0 -0
  123. {reader-3.15 → reader-3.17}/src/reader/_app/templates/tags.html +0 -0
  124. {reader-3.15 → reader-3.17}/src/reader/_app/wsgi.py +0 -0
  125. {reader-3.15 → reader-3.17}/src/reader/_cli.py +0 -0
  126. {reader-3.15 → reader-3.17}/src/reader/_config.py +0 -0
  127. {reader-3.15 → reader-3.17}/src/reader/_hash_utils.py +0 -0
  128. {reader-3.15 → reader-3.17}/src/reader/_parser/__init__.py +0 -0
  129. {reader-3.15 → reader-3.17}/src/reader/_parser/_http_utils.py +0 -0
  130. {reader-3.15 → reader-3.17}/src/reader/_parser/_lazy.py +0 -0
  131. {reader-3.15 → reader-3.17}/src/reader/_parser/_url_utils.py +0 -0
  132. {reader-3.15 → reader-3.17}/src/reader/_parser/file.py +0 -0
  133. {reader-3.15 → reader-3.17}/src/reader/_parser/http.py +0 -0
  134. {reader-3.15 → reader-3.17}/src/reader/_parser/jsonfeed.py +0 -0
  135. {reader-3.15 → reader-3.17}/src/reader/_parser/requests/__init__.py +0 -0
  136. {reader-3.15 → reader-3.17}/src/reader/_parser/requests/_lazy.py +0 -0
  137. {reader-3.15 → reader-3.17}/src/reader/_plugins/__init__.py +0 -0
  138. {reader-3.15 → reader-3.17}/src/reader/_plugins/enclosure_tags.py +0 -0
  139. {reader-3.15 → reader-3.17}/src/reader/_plugins/preview_feed_list.py +0 -0
  140. {reader-3.15 → reader-3.17}/src/reader/_plugins/share.py +0 -0
  141. {reader-3.15 → reader-3.17}/src/reader/_plugins/sqlite_releases.py +0 -0
  142. {reader-3.15 → reader-3.17}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  143. {reader-3.15 → reader-3.17}/src/reader/_plugins/timer.py +0 -0
  144. {reader-3.15 → reader-3.17}/src/reader/_storage/__init__.py +0 -0
  145. {reader-3.15 → reader-3.17}/src/reader/_storage/_changes.py +0 -0
  146. {reader-3.15 → reader-3.17}/src/reader/_storage/_feeds.py +0 -0
  147. {reader-3.15 → reader-3.17}/src/reader/_storage/_html_utils.py +0 -0
  148. {reader-3.15 → reader-3.17}/src/reader/_storage/_sql_utils.py +0 -0
  149. {reader-3.15 → reader-3.17}/src/reader/_storage/_tags.py +0 -0
  150. {reader-3.15 → reader-3.17}/src/reader/_vendor/__init__.py +0 -0
  151. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/__init__.py +0 -0
  152. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/api.py +0 -0
  153. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  154. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  155. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  156. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  157. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  158. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  159. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  160. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  161. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  162. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/encodings.py +0 -0
  163. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  164. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/html.py +0 -0
  165. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/http.py +0 -0
  166. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/mixin.py +0 -0
  167. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  168. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  169. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  170. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  171. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  172. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  173. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  174. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  175. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  176. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  177. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  178. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  179. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  180. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/py.typed +0 -0
  181. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  182. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/sgml.py +0 -0
  183. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/urls.py +0 -0
  184. {reader-3.15 → reader-3.17}/src/reader/_vendor/feedparser/util.py +0 -0
  185. {reader-3.15 → reader-3.17}/src/reader/exceptions.py +0 -0
  186. {reader-3.15 → reader-3.17}/src/reader/plugins/__init__.py +0 -0
  187. {reader-3.15 → reader-3.17}/src/reader/plugins/enclosure_dedupe.py +0 -0
  188. {reader-3.15 → reader-3.17}/src/reader/plugins/entry_dedupe.py +0 -0
  189. {reader-3.15 → reader-3.17}/src/reader/plugins/mark_as_read.py +0 -0
  190. {reader-3.15 → reader-3.17}/src/reader/plugins/readtime.py +0 -0
  191. {reader-3.15 → reader-3.17}/src/reader/plugins/ua_fallback.py +0 -0
  192. {reader-3.15 → reader-3.17}/src/reader/py.typed +0 -0
  193. {reader-3.15 → reader-3.17}/src/reader.egg-info/dependency_links.txt +0 -0
  194. {reader-3.15 → reader-3.17}/src/reader.egg-info/top_level.txt +0 -0
  195. {reader-3.15 → reader-3.17}/tests/data/10.json +0 -0
  196. {reader-3.15 → reader-3.17}/tests/data/10.json.py +0 -0
  197. {reader-3.15 → reader-3.17}/tests/data/custom +0 -0
  198. {reader-3.15 → reader-3.17}/tests/data/empty.atom +0 -0
  199. {reader-3.15 → reader-3.17}/tests/data/empty.atom.py +0 -0
  200. {reader-3.15 → reader-3.17}/tests/data/empty.json +0 -0
  201. {reader-3.15 → reader-3.17}/tests/data/empty.json.py +0 -0
  202. {reader-3.15 → reader-3.17}/tests/data/empty.rss +0 -0
  203. {reader-3.15 → reader-3.17}/tests/data/empty.rss.py +0 -0
  204. {reader-3.15 → reader-3.17}/tests/data/full.json +0 -0
  205. {reader-3.15 → reader-3.17}/tests/data/full.json.py +0 -0
  206. {reader-3.15 → reader-3.17}/tests/data/invalid.json +0 -0
  207. {reader-3.15 → reader-3.17}/tests/data/invalid.json.py +0 -0
  208. {reader-3.15 → reader-3.17}/tests/data/relative.atom +0 -0
  209. {reader-3.15 → reader-3.17}/tests/data/relative.atom.py +0 -0
  210. {reader-3.15 → reader-3.17}/tests/data/sqlite_releases.html +0 -0
  211. {reader-3.15 → reader-3.17}/tests/data/unknown.json +0 -0
  212. {reader-3.15 → reader-3.17}/tests/data/unknown.json.py +0 -0
  213. {reader-3.15 → reader-3.17}/tests/reader_methods.py +0 -0
  214. {reader-3.15 → reader-3.17}/tests/reader_test_plugins/__init__.py +0 -0
  215. {reader-3.15 → reader-3.17}/tests/reader_test_plugins/good.py +0 -0
  216. {reader-3.15 → reader-3.17}/tests/reader_test_plugins/init_error.py +0 -0
  217. {reader-3.15 → reader-3.17}/tests/reader_test_plugins/missing_dependency.py +0 -0
  218. {reader-3.15 → reader-3.17}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  219. /reader-3.15/tests/test_utils.py → /reader-3.17/tests/test__utils.py +0 -0
  220. {reader-3.15 → reader-3.17}/tests/test_app_wsgi.py +0 -0
  221. {reader-3.15 → reader-3.17}/tests/test_bench.py +0 -0
  222. {reader-3.15 → reader-3.17}/tests/test_config.py +0 -0
  223. {reader-3.15 → reader-3.17}/tests/test_exceptions.py +0 -0
  224. {reader-3.15 → reader-3.17}/tests/test_hash_utils.py +0 -0
  225. {reader-3.15 → reader-3.17}/tests/test_html_utils.py +0 -0
  226. {reader-3.15 → reader-3.17}/tests/test_lazy_imports.py +0 -0
  227. {reader-3.15 → reader-3.17}/tests/test_parser.py +0 -0
  228. {reader-3.15 → reader-3.17}/tests/test_plugins_preview_feed_list.py +0 -0
  229. {reader-3.15 → reader-3.17}/tests/test_plugins_sqlite_releases.py +0 -0
  230. {reader-3.15 → reader-3.17}/tests/test_plugins_ua_fallback.py +0 -0
  231. {reader-3.15 → reader-3.17}/tests/test_reader_counts.py +0 -0
  232. {reader-3.15 → reader-3.17}/tests/test_reader_deprecations.py +0 -0
  233. {reader-3.15 → reader-3.17}/tests/test_reader_plugins.py +0 -0
  234. {reader-3.15 → reader-3.17}/tests/test_search.py +0 -0
  235. {reader-3.15 → reader-3.17}/tests/test_sql_utils.py +0 -0
  236. {reader-3.15 → reader-3.17}/tests/test_sqlite_utils.py +0 -0
  237. {reader-3.15 → reader-3.17}/tests/test_test_utils.py +0 -0
@@ -6,6 +6,90 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.17
10
+ ------------
11
+
12
+ Released 2025-03-08
13
+
14
+ .. attention::
15
+
16
+ This is the last release to support Python 3.10;
17
+ see :issue:`354` for details.
18
+
19
+ * Support PyPy 3.11. (:issue:`354`)
20
+
21
+ * Start working on a web app :ref:`re-design <app roadmap>`
22
+ based on `htmx`_ and `Bootstrap`_,
23
+ beginning with a new version of the main entries page,
24
+ and dark mode support;
25
+ some :ref:`screenshots <app screenshots>`.
26
+ (:issue:`318`)
27
+
28
+ * :attr:`Entry.feed_resolved_title`:
29
+ return both the source and feed titles only if they are different.
30
+
31
+ * The :mod:`~reader._plugins.cli_status` plugin
32
+ now creates only one entry per command,
33
+ with the newest run first.
34
+
35
+ .. _htmx: https://htmx.org/
36
+ .. _Bootstrap: https://getbootstrap.com/
37
+
38
+
39
+ Version 3.16
40
+ ------------
41
+
42
+ Released 2024-12-08
43
+
44
+ * Parse and store :attr:`Entry.source` for Atom and RSS feeds. (:issue:`267`)
45
+
46
+ * Add :attr:`Entry.feed_resolved_title` and :attr:`Feed.resolved_title` properties.
47
+ * The ``feed`` search column now indexes :attr:`Entry.feed_resolved_title`,
48
+ instead of feed :attr:`~Feed.user_title` or :attr:`~Feed.title`.
49
+ * Allow filtering entries by the entry source.
50
+
51
+ * Add :func:`reader.utils.archive_entries` for
52
+ copying entries to an "archived" feed.
53
+ (:issue:`290`)
54
+
55
+ * Add :meth:`~Reader.copy_entry`.
56
+ * Allow archiving entries from the web app.
57
+
58
+ * Fix bug causing :class:`Reader` operations
59
+ from a thread other than the one that created the instance
60
+ to happen with foreign key constraint enforcement disabled
61
+ (e.g. deleting a feed from another thread would not delete its entries).
62
+
63
+ This bug exists since using :class:`Reader` instances from other threads
64
+ became allowed in `2.15 <Version 2.15_>`_.
65
+
66
+ Serving the web application with ``python -m reader serve``
67
+ is known to be affected.
68
+ Serving it with uWSGI without threads (the default)
69
+ should not be affected.
70
+
71
+ .. attention::
72
+
73
+ **Your database may be in an inconsistent state because of this bug.**
74
+
75
+ It is recommended you run `PRAGMA foreign_key_check`_ on your database.
76
+
77
+ If you are upgrading from a version prior to 3.16
78
+ (i.e. were not using a pre-release version of *reader*),
79
+ the migration will do so for you.
80
+ If there are inconsistencies, you will get this error::
81
+
82
+ StorageError: integrity error: after migrating to version 43:
83
+ integrity error: FOREIGN KEY constraint failed
84
+
85
+ * Fix :meth:`~Reader.enable_search` / :meth:`~Reader.update_search`
86
+ not working when the search database is missing but change tracking is enabled
87
+ (e.g. when restoring the main database from backup).
88
+ (:issue:`362`)
89
+
90
+ .. _PRAGMA foreign_key_check: https://www.sqlite.org/pragma.html#pragma_foreign_key_check
91
+
92
+
9
93
  Version 3.15
10
94
  ------------
11
95
 
@@ -1,5 +1,6 @@
1
1
  recursive-include src/reader/_app/templates *
2
2
  recursive-include src/reader/_app/static *
3
+ recursive-include src/reader/_app/v2 *
3
4
  recursive-include src/reader/_plugins/templates *
4
5
  recursive-include src/reader *.pyi
5
6
  recursive-include src/reader py.typed
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: reader
3
- Version: 3.15
3
+ Version: 3.17
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -50,6 +50,8 @@ Provides-Extra: app
50
50
  Requires-Dist: flask>=0.10; extra == "app"
51
51
  Requires-Dist: humanize!=4.7.*,>=4; extra == "app"
52
52
  Requires-Dist: PyYAML; extra == "app"
53
+ Requires-Dist: WTForms; extra == "app"
54
+ Requires-Dist: jinja2-fragments; extra == "app"
53
55
  Provides-Extra: unstable-plugins
54
56
  Requires-Dist: requests; extra == "unstable-plugins"
55
57
  Requires-Dist: mutagen; extra == "unstable-plugins"
Binary file
@@ -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
@@ -67,9 +67,34 @@ If running on a personal computer, you can use cron to run ``serve`` at boot::
67
67
  .. _Werkzeug's development server: http://werkzeug.pocoo.org/docs/0.14/serving/#werkzeug.serving.run_simple
68
68
 
69
69
 
70
+ .. _app screenshots:
71
+
70
72
  Screenshots
71
73
  -----------
72
74
 
75
+ Following are screenshots of the web app re-design
76
+ mentioned in the :ref:`roadmap <app roadmap>`.
77
+ For the legacy web app, see `Screenshots (legacy)`_.
78
+
79
+ Main page
80
+ ~~~~~~~~~
81
+
82
+ .. figure:: screenshots/entries-v2-dark.png
83
+ :width: 240px
84
+
85
+ main page (dark mode)
86
+
87
+ .. figure:: screenshots/entries-v2-filters-light.png
88
+ :width: 240px
89
+
90
+ main page – more filters (light mode)
91
+
92
+
93
+ Screenshots (legacy)
94
+ --------------------
95
+
96
+ Following are screenshots of the original (legacy) web app.
97
+
73
98
  Main page
74
99
  ~~~~~~~~~
75
100
 
@@ -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>`
@@ -120,8 +122,14 @@ Long term, I'd like to:
120
122
  * switch to `htmx`_ instead of using a home-grown solution
121
123
  * spin it off into a separate package/project
122
124
 
123
- .. _htmx: https://htmx.org/
125
+ **2025 update**:
126
+ I've started working on a re-design based on `htmx`_ and `Bootstrap`_;
127
+ some :ref:`screenshots <app screenshots>`.
128
+ The new app will be available in parallel with the old one
129
+ until it reaches feature parity.
124
130
 
131
+ .. _htmx: https://htmx.org/
132
+ .. _Bootstrap: https://getbootstrap.com/
125
133
 
126
134
 
127
135
  .. _compat:
@@ -360,6 +368,8 @@ Speeding up ``get_entry_counts(feed=...)``:
360
368
  * having an index on entries(feed) yielded a 4x improvement: :issue:`251`
361
369
  * even better, we should cache commonly-used counts: :issue:`306#issuecomment-1694655504`
362
370
 
371
+ Official guidance: https://www.sqlite.org/queryplanner-ng.html#howtofix
372
+
363
373
 
364
374
  Parser
365
375
  ~~~~~~
@@ -512,6 +522,16 @@ From the initial issue:
512
522
  * use cases: :issue:`149#issuecomment-700066794`
513
523
  * initial requirements: :issue:`149#issuecomment-700532183`
514
524
 
525
+ Splitting a feed into two feeds is discussed in :issue:`221`.
526
+ The main takeaway at the time (2021) was that
527
+ not all entry data can be retrieved through the storage API,
528
+ so it's not fully possible to round-trip an entry.
529
+ Based on a quick 2024 survey,
530
+ the only affected attributes
531
+ are :attr:`~.EntryUpdateIntent.feed_order` (can't get; not critical)
532
+ and :attr:`~Entry.original_feed_url` (can't set; will be required for
533
+ archiving important entries of deleted feeds, :issue:`290`).
534
+
515
535
 
516
536
  Resource tags / metadata
517
537
  ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -651,6 +671,8 @@ Why I want to postpone batch update/set methods:
651
671
  tl:dr: Performance is likely a non-issue with SQLite,
652
672
  convenience can be added on top as a plugin.
653
673
 
674
+ (2025) Why it may be worth adding batch interfaces anyway (even if underneath the storage implementation doesn't actually batch) – it allows for future optimization: https://blog.glyph.im/2022/12/potato-programming.html
675
+
654
676
  See the 2.12 reader._app.ResourceTags class for an idea of how to
655
677
  represent a bunch of tags in a reserved-name-scheme-agnostic way
656
678
  (useful e.g. for when get_entries() should return tags x, y, z of each entry).
@@ -708,16 +730,16 @@ Wrapping underlying storage exceptions
708
730
 
709
731
  Which exception to wrap, and which not: :issue:`21#issuecomment-365442439`.
710
732
 
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
733
 
716
734
  Timezone handling
717
735
  ~~~~~~~~~~~~~~~~~
718
736
 
719
737
  Aware vs. naive, and what's needed to go fully aware: :issue:`233#issuecomment-881618002`.
720
738
 
739
+ In version 3.10 (November 2023), all internal APIs were changed
740
+ to use timezone-aware datetimes, with the timezone set to UTC,
741
+ in preparation for support for any timezone.
742
+
721
743
 
722
744
  OPML support
723
745
  ~~~~~~~~~~~~
@@ -77,6 +77,9 @@ app = [
77
77
  "humanize>=4,!=4.7.*",
78
78
  # for config
79
79
  "PyYAML",
80
+ # for v2
81
+ "WTForms",
82
+ "jinja2-fragments",
80
83
  ]
81
84
 
82
85
  # UNSTABLE PLUGINS
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.15'
40
+ __version__ = '3.17'
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
@@ -104,6 +105,7 @@ def get_reader():
104
105
  def stream_template(template_name_or_list, **kwargs):
105
106
  template = current_app.jinja_env.get_template(template_name_or_list)
106
107
  stream = template.stream(**kwargs)
108
+ # TODO: increase to at least 1-2k, like this we have 50% overhead
107
109
  stream.enable_buffering(50)
108
110
  return Response(stream_with_context(stream))
109
111
 
@@ -157,12 +159,8 @@ class EntryProxy:
157
159
  def title(self):
158
160
  highlight = self._search_result.metadata.get('.title')
159
161
  if highlight:
160
- return str(highlight)
161
- return None
162
-
163
- @property
164
- def feed(self):
165
- return FeedProxy(self._search_result, self._entry)
162
+ return highlighted(highlight)
163
+ return self._entry.title
166
164
 
167
165
  @property
168
166
  def summary(self):
@@ -184,21 +182,12 @@ class EntryProxy:
184
182
  def get_content(self, prefer_summary=False):
185
183
  return _get_entry_content(self, prefer_summary)
186
184
 
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
185
  @property
197
- def title(self):
198
- highlight = self._search_result.metadata.get('.feed.title')
186
+ def feed_resolved_title(self):
187
+ highlight = self._search_result.metadata.get('.feed_resolved_title')
199
188
  if highlight:
200
- return str(highlight)
201
- return self._entry.feed.title
189
+ return highlighted(highlight)
190
+ return self._entry.feed_resolved_url
202
191
 
203
192
 
204
193
  @dataclass
@@ -643,6 +632,14 @@ def mark_all_as_unread(data):
643
632
  get_reader().mark_entry_as_unread((feed_url, entry_id))
644
633
 
645
634
 
635
+ @form_api(really=True)
636
+ @readererror_to_apierror()
637
+ def archive_all(data):
638
+ feed_url = data['feed-url']
639
+ entry_ids = json.loads(data['entry-id'])
640
+ archive_entries(get_reader(), [(feed_url, eid) for eid in entry_ids])
641
+
642
+
646
643
  @form_api
647
644
  @readererror_to_apierror()
648
645
  def mark_as_important(data):
@@ -840,6 +837,10 @@ def create_app(config):
840
837
 
841
838
  app.register_blueprint(blueprint)
842
839
 
840
+ from . import v2
841
+
842
+ app.register_blueprint(v2.blueprint, url_prefix='/v2')
843
+
843
844
  # NOTE: this is part of the app extension API
844
845
  app.reader_additional_enclosure_links = []
845
846
  app.reader_additional_links = []
@@ -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) }}">{{ macros.feed_title(entry.feed) }}</a>
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) }}">{{ macros.feed_title(entry.feed) }}</a>
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>
@@ -21,6 +21,7 @@ window.onload = function () {
21
21
  <a href="{{ url_for('reader.feeds') }}">feeds</a>
22
22
  <a href="{{ url_for('reader.tags') }}">tags</a>
23
23
  <a href="{{ url_for('reader.metadata') }}">metadata</a>
24
+ <a href="{{ url_for('v2.entries') }}">v2</a>
24
25
 
25
26
  {{ macros.text_input_button_get(
26
27
  'reader.preview', 'add feed', 'url', 'url',
@@ -6,17 +6,11 @@
6
6
  {%- endmacro %}
7
7
 
8
8
  {% macro feed_title(feed) %}
9
- {%- if feed.user_title and feed.user_title.strip() -%}
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.user_title and feed.user_title.strip() and feed.title and feed.title.strip() -%}
13
+ {%- if feed.title and feed.resolved_title != feed.title -%}
20
14
  {{ feed.title }}
21
15
  {%- endif -%}
22
16
  {% endmacro %}
@@ -0,0 +1,123 @@
1
+ import itertools
2
+ from functools import partial
3
+
4
+ from flask import abort
5
+ from flask import Blueprint
6
+ from flask import current_app
7
+ from flask import redirect
8
+ from flask import request
9
+ from flask import url_for
10
+ from jinja2_fragments.flask import render_block
11
+
12
+ from reader import InvalidSearchQueryError
13
+
14
+ from .. import EntryProxy
15
+ from .. import get_reader
16
+ from .. import stream_template
17
+ from .forms import EntryFilter
18
+ from .forms import SearchEntryFilter
19
+
20
+
21
+ blueprint = Blueprint(
22
+ 'v2', __name__, template_folder='templates', static_folder='static'
23
+ )
24
+
25
+
26
+ @blueprint.route('/')
27
+ def entries():
28
+ reader = get_reader()
29
+
30
+ # TODO: search improvements
31
+ # TODO: paqgination
32
+ # TODO: read time
33
+
34
+ if request.args.get('q', '').strip():
35
+ form = SearchEntryFilter(request.args)
36
+ else:
37
+ form = EntryFilter(request.args)
38
+
39
+ form_args = form.args
40
+ if q := form_args.pop('Q', ''):
41
+ form_args['q'] = q
42
+ return redirect(url_for('.entries', **form_args))
43
+ if form_args != request.args.to_dict():
44
+ return redirect(url_for('.entries', **form_args))
45
+
46
+ feed = None
47
+ if form.feed.data:
48
+ feed = reader.get_feed(form.feed.data, None)
49
+ if not feed:
50
+ abort(404)
51
+
52
+ kwargs = dict(form.data)
53
+ if query := kwargs.pop('search', None):
54
+
55
+ def get_entries(**kwargs):
56
+ for sr in reader.search_entries(query, **kwargs):
57
+ yield EntryProxy(sr, reader.get_entry(sr))
58
+
59
+ else:
60
+ get_entries = reader.get_entries
61
+
62
+ entries = []
63
+ if form.validate():
64
+ try:
65
+ entries = eager_iterator(get_entries(**kwargs, limit=64))
66
+ except StopIteration:
67
+ pass
68
+ except InvalidSearchQueryError as e:
69
+ form.search.errors.append(f"invalid query: {e}")
70
+
71
+ return stream_template(
72
+ 'v2/entries.html',
73
+ form=form,
74
+ entries=entries,
75
+ feed=feed,
76
+ )
77
+
78
+
79
+ def eager_iterator(it):
80
+ it = iter(it)
81
+ try:
82
+ return itertools.chain([next(it)], it)
83
+ except StopIteration:
84
+ return it
85
+
86
+
87
+ @blueprint.route('/mark-as', methods=['POST'])
88
+ def mark_as():
89
+ reader = get_reader()
90
+
91
+ entry = request.form['feed-url'], request.form['entry-id']
92
+
93
+ if 'read' in request.form:
94
+ match request.form['read']:
95
+ case 'true':
96
+ reader.set_entry_read(entry, True)
97
+ case 'false':
98
+ reader.set_entry_read(entry, False)
99
+ case _:
100
+ abort(422)
101
+
102
+ if 'important' in request.form:
103
+ match request.form['important']:
104
+ case 'true':
105
+ reader.set_entry_important(entry, True)
106
+ case 'false':
107
+ reader.set_entry_important(entry, False)
108
+ case 'none':
109
+ reader.set_entry_important(entry, None)
110
+ case _:
111
+ abort(422)
112
+
113
+ if request.headers.get('hx-request') == 'true':
114
+ return render_block(
115
+ 'v2/entries.html',
116
+ 'entry_form',
117
+ entry=reader.get_entry(entry),
118
+ next=request.form['next'],
119
+ # equivalent to {% import "v2/macros.html" as macros %}
120
+ macros=current_app.jinja_env.get_template('v2/macros.html').module,
121
+ )
122
+
123
+ return redirect(request.form['next'], code=303)