reader 3.24__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.
Files changed (267) hide show
  1. {reader-3.24 → reader-3.26}/CHANGES.rst +36 -0
  2. {reader-3.24/src/reader.egg-info → reader-3.26}/PKG-INFO +4 -1
  3. {reader-3.24 → reader-3.26}/README.rst +1 -0
  4. {reader-3.24 → reader-3.26}/docs/api.rst +6 -0
  5. {reader-3.24 → reader-3.26}/docs/app.rst +16 -0
  6. {reader-3.24 → reader-3.26}/docs/conf.py +1 -1
  7. {reader-3.24 → reader-3.26}/docs/guide.rst +10 -7
  8. {reader-3.24 → reader-3.26}/docs/install.rst +8 -6
  9. {reader-3.24 → reader-3.26}/docs/internal.rst +29 -22
  10. {reader-3.24 → reader-3.26}/docs/plugins.rst +1 -0
  11. reader-3.26/docs/screenshots/autodiscovery.png +0 -0
  12. reader-3.26/docs/screenshots/export-database.png +0 -0
  13. {reader-3.24 → reader-3.26}/pyproject.toml +5 -1
  14. {reader-3.24 → reader-3.26}/src/reader/__init__.py +1 -1
  15. {reader-3.24 → reader-3.26}/src/reader/_app/__init__.py +71 -0
  16. {reader-3.24 → reader-3.26}/src/reader/_app/cli.py +12 -1
  17. reader-3.26/src/reader/_app/exports.py +58 -0
  18. {reader-3.24 → reader-3.26}/src/reader/_app/ext.py +29 -2
  19. {reader-3.24 → reader-3.26}/src/reader/_app/static/style.css +2 -4
  20. {reader-3.24 → reader-3.26}/src/reader/_app/templates/add_feed.html +9 -1
  21. {reader-3.24 → reader-3.26}/src/reader/_app/templates/entries.html +58 -17
  22. {reader-3.24 → reader-3.26}/src/reader/_app/templates/entry.html +1 -2
  23. {reader-3.24 → reader-3.26}/src/reader/_app/templates/help.html +8 -2
  24. {reader-3.24 → reader-3.26}/src/reader/_app/templates/layout_base.html +18 -2
  25. {reader-3.24 → reader-3.26}/src/reader/_app/templates/macros.html +13 -4
  26. reader-3.26/src/reader/_app/templates/settings.html +1 -0
  27. reader-3.26/src/reader/_app/templates/settings_base.html +67 -0
  28. {reader-3.24 → reader-3.26}/src/reader/_cli.py +12 -0
  29. reader-3.24/src/reader/_parser/_lazy.py → reader-3.26/src/reader/_parser/__init__.py +481 -26
  30. {reader-3.24 → reader-3.26}/src/reader/_parser/feedparser.py +8 -2
  31. reader-3.26/src/reader/_parser/http.py +299 -0
  32. {reader-3.24 → reader-3.26}/src/reader/_parser/jsonfeed.py +6 -2
  33. {reader-3.24 → reader-3.26}/src/reader/_plugins/legacy/preview_feed_list.py +13 -8
  34. {reader-3.24 → reader-3.26}/src/reader/_plugins/sqlite_releases.py +4 -2
  35. {reader-3.24 → reader-3.26}/src/reader/_storage/_base.py +31 -1
  36. {reader-3.24 → reader-3.26}/src/reader/_storage/_entries.py +1 -0
  37. {reader-3.24 → reader-3.26}/src/reader/_storage/_html_utils.py +9 -4
  38. {reader-3.24 → reader-3.26}/src/reader/_storage/_schema.py +1 -1
  39. {reader-3.24 → reader-3.26}/src/reader/_types.py +14 -1
  40. {reader-3.24 → reader-3.26}/src/reader/_update/base.py +14 -13
  41. {reader-3.24 → reader-3.26}/src/reader/_update/hooks.py +34 -20
  42. {reader-3.24 → reader-3.26}/src/reader/_utils.py +0 -15
  43. {reader-3.24 → reader-3.26}/src/reader/core.py +20 -16
  44. reader-3.26/src/reader/discover.py +143 -0
  45. reader-3.26/src/reader/plugins/autodiscover.py +91 -0
  46. {reader-3.24 → reader-3.26}/src/reader/plugins/ua_fallback.py +4 -2
  47. {reader-3.24 → reader-3.26/src/reader.egg-info}/PKG-INFO +4 -1
  48. {reader-3.24 → reader-3.26}/src/reader.egg-info/SOURCES.txt +9 -4
  49. {reader-3.24 → reader-3.26}/src/reader.egg-info/requires.txt +2 -0
  50. {reader-3.24 → reader-3.26}/tests/fakeparser.py +1 -2
  51. {reader-3.24 → reader-3.26}/tests/test_cli.py +4 -1
  52. reader-3.26/tests/test_discover.py +48 -0
  53. {reader-3.24 → reader-3.26}/tests/test_html_utils.py +1 -1
  54. {reader-3.24 → reader-3.26}/tests/test_lazy_imports.py +1 -1
  55. {reader-3.24 → reader-3.26}/tests/test_parser.py +34 -17
  56. reader-3.26/tests/test_plugins_autodiscover.py +69 -0
  57. {reader-3.24 → reader-3.26}/tests/test_reader.py +2 -2
  58. {reader-3.24 → reader-3.26}/tests/test_reader_hooks.py +18 -1
  59. {reader-3.24 → reader-3.26}/tests/test_storage.py +32 -7
  60. reader-3.24/scripts/backup.sh +0 -36
  61. reader-3.24/src/reader/_parser/__init__.py +0 -510
  62. reader-3.24/src/reader/_parser/http.py +0 -107
  63. reader-3.24/src/reader/_parser/requests/__init__.py +0 -176
  64. reader-3.24/src/reader/_parser/requests/_lazy.py +0 -162
  65. {reader-3.24 → reader-3.26}/LICENSE +0 -0
  66. {reader-3.24 → reader-3.26}/MANIFEST.in +0 -0
  67. {reader-3.24 → reader-3.26}/docs/changelog.rst +0 -0
  68. {reader-3.24 → reader-3.26}/docs/cli.rst +0 -0
  69. {reader-3.24 → reader-3.26}/docs/config.rst +0 -0
  70. {reader-3.24 → reader-3.26}/docs/contributing.rst +0 -0
  71. {reader-3.24 → reader-3.26}/docs/dev.rst +0 -0
  72. {reader-3.24 → reader-3.26}/docs/index.rst +0 -0
  73. {reader-3.24 → reader-3.26}/docs/screenshots/empty.png +0 -0
  74. {reader-3.24 → reader-3.26}/docs/screenshots/entries-filters-light.png +0 -0
  75. {reader-3.24 → reader-3.26}/docs/screenshots/entries.png +0 -0
  76. {reader-3.24 → reader-3.26}/docs/screenshots/entry-image-light.png +0 -0
  77. {reader-3.24 → reader-3.26}/docs/screenshots/entry.png +0 -0
  78. {reader-3.24 → reader-3.26}/docs/screenshots/feed.png +0 -0
  79. {reader-3.24 → reader-3.26}/docs/screenshots/feeds.png +0 -0
  80. {reader-3.24 → reader-3.26}/docs/screenshots/help.png +0 -0
  81. {reader-3.24 → reader-3.26}/docs/screenshots/import-result.png +0 -0
  82. {reader-3.24 → reader-3.26}/docs/screenshots/import-select.png +0 -0
  83. {reader-3.24 → reader-3.26}/docs/tutorial.rst +0 -0
  84. {reader-3.24 → reader-3.26}/docs/why.rst +0 -0
  85. {reader-3.24 → reader-3.26}/examples/config.toml +0 -0
  86. {reader-3.24 → reader-3.26}/examples/custom_headers.py +0 -0
  87. {reader-3.24 → reader-3.26}/examples/feed_slugs.py +0 -0
  88. {reader-3.24 → reader-3.26}/examples/parser_only.py +0 -0
  89. {reader-3.24 → reader-3.26}/examples/podcast.py +0 -0
  90. {reader-3.24 → reader-3.26}/examples/terminal.py +0 -0
  91. {reader-3.24 → reader-3.26}/run.sh +0 -0
  92. {reader-3.24 → reader-3.26}/scripts/bench.py +0 -0
  93. {reader-3.24 → reader-3.26}/scripts/debug_storage_stats.py +0 -0
  94. {reader-3.24 → reader-3.26}/scripts/entry_dedupe_backtest.py +0 -0
  95. {reader-3.24 → reader-3.26}/scripts/jscontrols.html +0 -0
  96. {reader-3.24 → reader-3.26}/scripts/jscontrols.py +0 -0
  97. {reader-3.24 → reader-3.26}/scripts/lines.sh +0 -0
  98. {reader-3.24 → reader-3.26}/scripts/release.py +0 -0
  99. {reader-3.24 → reader-3.26}/scripts/vendor_app_stuff.py +0 -0
  100. {reader-3.24 → reader-3.26}/setup.cfg +0 -0
  101. {reader-3.24 → reader-3.26}/src/reader/__main__.py +0 -0
  102. {reader-3.24 → reader-3.26}/src/reader/_app/forms.py +0 -0
  103. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/__init__.py +0 -0
  104. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/api_thing.py +0 -0
  105. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/static/controls.js +0 -0
  106. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/static/style.css +0 -0
  107. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  108. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/entries.html +0 -0
  109. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/entry.html +0 -0
  110. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/feeds.html +0 -0
  111. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/layout.html +0 -0
  112. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/macros.html +0 -0
  113. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/metadata.html +0 -0
  114. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/templates/tags.html +0 -0
  115. {reader-3.24 → reader-3.26}/src/reader/_app/legacy/wsgi.py +0 -0
  116. {reader-3.24 → reader-3.26}/src/reader/_app/static/theme.js +0 -0
  117. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/README +0 -0
  118. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  119. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  120. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  121. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  122. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  123. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  124. {reader-3.24 → reader-3.26}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  125. {reader-3.24 → reader-3.26}/src/reader/_app/templates/change_feed_title.html +0 -0
  126. {reader-3.24 → reader-3.26}/src/reader/_app/templates/delete_feed.html +0 -0
  127. {reader-3.24 → reader-3.26}/src/reader/_app/templates/feeds.html +0 -0
  128. {reader-3.24 → reader-3.26}/src/reader/_app/templates/import_feeds.html +0 -0
  129. {reader-3.24 → reader-3.26}/src/reader/_app/templates/layout.html +0 -0
  130. {reader-3.24 → reader-3.26}/src/reader/_app/wsgi.py +0 -0
  131. {reader-3.24 → reader-3.26}/src/reader/_config_utils.py +0 -0
  132. {reader-3.24 → reader-3.26}/src/reader/_hash_utils.py +0 -0
  133. {reader-3.24 → reader-3.26}/src/reader/_logging.py +0 -0
  134. {reader-3.24 → reader-3.26}/src/reader/_parser/_http_utils.py +0 -0
  135. {reader-3.24 → reader-3.26}/src/reader/_parser/_url_utils.py +0 -0
  136. {reader-3.24 → reader-3.26}/src/reader/_parser/file.py +0 -0
  137. {reader-3.24 → reader-3.26}/src/reader/_plugins/__init__.py +0 -0
  138. {reader-3.24 → reader-3.26}/src/reader/_plugins/cli_status.py +0 -0
  139. {reader-3.24 → reader-3.26}/src/reader/_plugins/legacy/__init__.py +0 -0
  140. {reader-3.24 → reader-3.26}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
  141. {reader-3.24 → reader-3.26}/src/reader/_plugins/legacy/share.py +0 -0
  142. {reader-3.24 → reader-3.26}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  143. {reader-3.24 → reader-3.26}/src/reader/_plugins/timer.py +0 -0
  144. {reader-3.24 → reader-3.26}/src/reader/_storage/__init__.py +0 -0
  145. {reader-3.24 → reader-3.26}/src/reader/_storage/_changes.py +0 -0
  146. {reader-3.24 → reader-3.26}/src/reader/_storage/_feeds.py +0 -0
  147. {reader-3.24 → reader-3.26}/src/reader/_storage/_search.py +0 -0
  148. {reader-3.24 → reader-3.26}/src/reader/_storage/_sql_utils.py +0 -0
  149. {reader-3.24 → reader-3.26}/src/reader/_storage/_sqlite_utils.py +0 -0
  150. {reader-3.24 → reader-3.26}/src/reader/_storage/_tags.py +0 -0
  151. {reader-3.24 → reader-3.26}/src/reader/_update/__init__.py +0 -0
  152. {reader-3.24 → reader-3.26}/src/reader/_vendor/__init__.py +0 -0
  153. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/__init__.py +0 -0
  154. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/api.py +0 -0
  155. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  156. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  157. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  158. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  159. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  160. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  161. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  162. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  163. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  164. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/encodings.py +0 -0
  165. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  166. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/html.py +0 -0
  167. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/http.py +0 -0
  168. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/mixin.py +0 -0
  169. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  170. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  171. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  172. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  173. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  174. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  175. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  176. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  177. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  178. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  179. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  180. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  181. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  182. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/py.typed +0 -0
  183. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  184. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/sgml.py +0 -0
  185. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/urls.py +0 -0
  186. {reader-3.24 → reader-3.26}/src/reader/_vendor/feedparser/util.py +0 -0
  187. {reader-3.24 → reader-3.26}/src/reader/exceptions.py +0 -0
  188. {reader-3.24 → reader-3.26}/src/reader/opml.py +0 -0
  189. {reader-3.24 → reader-3.26}/src/reader/plugins/__init__.py +0 -0
  190. {reader-3.24 → reader-3.26}/src/reader/plugins/_loader.py +0 -0
  191. {reader-3.24 → reader-3.26}/src/reader/plugins/enclosure_dedupe.py +0 -0
  192. {reader-3.24 → reader-3.26}/src/reader/plugins/entry_dedupe.py +0 -0
  193. {reader-3.24 → reader-3.26}/src/reader/plugins/mark_as_read.py +0 -0
  194. {reader-3.24 → reader-3.26}/src/reader/plugins/readtime.py +0 -0
  195. {reader-3.24 → reader-3.26}/src/reader/py.typed +0 -0
  196. {reader-3.24 → reader-3.26}/src/reader/types.py +0 -0
  197. {reader-3.24 → reader-3.26}/src/reader/utils.py +0 -0
  198. {reader-3.24 → reader-3.26}/src/reader.egg-info/dependency_links.txt +0 -0
  199. {reader-3.24 → reader-3.26}/src/reader.egg-info/top_level.txt +0 -0
  200. {reader-3.24 → reader-3.26}/tests/_plugins/__init__.py +0 -0
  201. {reader-3.24 → reader-3.26}/tests/_plugins/builtin.py +0 -0
  202. {reader-3.24 → reader-3.26}/tests/_plugins/error_during_import.py +0 -0
  203. {reader-3.24 → reader-3.26}/tests/_plugins/error_import_error.py +0 -0
  204. {reader-3.24 → reader-3.26}/tests/conftest.py +0 -0
  205. {reader-3.24 → reader-3.26}/tests/data/10.json +0 -0
  206. {reader-3.24 → reader-3.26}/tests/data/10.json.py +0 -0
  207. {reader-3.24 → reader-3.26}/tests/data/custom +0 -0
  208. {reader-3.24 → reader-3.26}/tests/data/empty.atom +0 -0
  209. {reader-3.24 → reader-3.26}/tests/data/empty.atom.py +0 -0
  210. {reader-3.24 → reader-3.26}/tests/data/empty.json +0 -0
  211. {reader-3.24 → reader-3.26}/tests/data/empty.json.py +0 -0
  212. {reader-3.24 → reader-3.26}/tests/data/empty.rss +0 -0
  213. {reader-3.24 → reader-3.26}/tests/data/empty.rss.py +0 -0
  214. {reader-3.24 → reader-3.26}/tests/data/full.atom +0 -0
  215. {reader-3.24 → reader-3.26}/tests/data/full.atom.py +0 -0
  216. {reader-3.24 → reader-3.26}/tests/data/full.json +0 -0
  217. {reader-3.24 → reader-3.26}/tests/data/full.json.py +0 -0
  218. {reader-3.24 → reader-3.26}/tests/data/full.rss +0 -0
  219. {reader-3.24 → reader-3.26}/tests/data/full.rss.py +0 -0
  220. {reader-3.24 → reader-3.26}/tests/data/invalid.json +0 -0
  221. {reader-3.24 → reader-3.26}/tests/data/invalid.json.py +0 -0
  222. {reader-3.24 → reader-3.26}/tests/data/relative.atom +0 -0
  223. {reader-3.24 → reader-3.26}/tests/data/relative.atom.py +0 -0
  224. {reader-3.24 → reader-3.26}/tests/data/relative.rss +0 -0
  225. {reader-3.24 → reader-3.26}/tests/data/relative.rss.py +0 -0
  226. {reader-3.24 → reader-3.26}/tests/data/sqlite_releases.html +0 -0
  227. {reader-3.24 → reader-3.26}/tests/data/unknown.json +0 -0
  228. {reader-3.24 → reader-3.26}/tests/data/unknown.json.py +0 -0
  229. {reader-3.24 → reader-3.26}/tests/fixtures.py +0 -0
  230. {reader-3.24 → reader-3.26}/tests/reader_methods.py +0 -0
  231. {reader-3.24 → reader-3.26}/tests/test__types.py +0 -0
  232. {reader-3.24 → reader-3.26}/tests/test__utils.py +0 -0
  233. {reader-3.24 → reader-3.26}/tests/test_app_legacy.py +0 -0
  234. {reader-3.24 → reader-3.26}/tests/test_app_wsgi.py +0 -0
  235. {reader-3.24 → reader-3.26}/tests/test_bench.py +0 -0
  236. {reader-3.24 → reader-3.26}/tests/test_changes.py +0 -0
  237. {reader-3.24 → reader-3.26}/tests/test_config_utils.py +0 -0
  238. {reader-3.24 → reader-3.26}/tests/test_exceptions.py +0 -0
  239. {reader-3.24 → reader-3.26}/tests/test_hash_utils.py +0 -0
  240. {reader-3.24 → reader-3.26}/tests/test_logging.py +0 -0
  241. {reader-3.24 → reader-3.26}/tests/test_opml.py +0 -0
  242. {reader-3.24 → reader-3.26}/tests/test_plugins_cli_status.py +0 -0
  243. {reader-3.24 → reader-3.26}/tests/test_plugins_enclosure_dedupe.py +0 -0
  244. {reader-3.24 → reader-3.26}/tests/test_plugins_entry_dedupe.py +0 -0
  245. {reader-3.24 → reader-3.26}/tests/test_plugins_loader.py +0 -0
  246. {reader-3.24 → reader-3.26}/tests/test_plugins_mark_as_read.py +0 -0
  247. {reader-3.24 → reader-3.26}/tests/test_plugins_preview_feed_list.py +0 -0
  248. {reader-3.24 → reader-3.26}/tests/test_plugins_readtime.py +0 -0
  249. {reader-3.24 → reader-3.26}/tests/test_plugins_sqlite_releases.py +0 -0
  250. {reader-3.24 → reader-3.26}/tests/test_plugins_ua_fallback.py +0 -0
  251. {reader-3.24 → reader-3.26}/tests/test_reader_counts.py +0 -0
  252. {reader-3.24 → reader-3.26}/tests/test_reader_deprecations.py +0 -0
  253. {reader-3.24 → reader-3.26}/tests/test_reader_filter.py +0 -0
  254. {reader-3.24 → reader-3.26}/tests/test_reader_integration.py +0 -0
  255. {reader-3.24 → reader-3.26}/tests/test_reader_lifecycle.py +0 -0
  256. {reader-3.24 → reader-3.26}/tests/test_reader_private.py +0 -0
  257. {reader-3.24 → reader-3.26}/tests/test_reader_search.py +0 -0
  258. {reader-3.24 → reader-3.26}/tests/test_reader_sort.py +0 -0
  259. {reader-3.24 → reader-3.26}/tests/test_reader_update.py +0 -0
  260. {reader-3.24 → reader-3.26}/tests/test_reader_utils.py +0 -0
  261. {reader-3.24 → reader-3.26}/tests/test_search.py +0 -0
  262. {reader-3.24 → reader-3.26}/tests/test_sql_utils.py +0 -0
  263. {reader-3.24 → reader-3.26}/tests/test_sqlite_utils.py +0 -0
  264. {reader-3.24 → reader-3.26}/tests/test_tags.py +0 -0
  265. {reader-3.24 → reader-3.26}/tests/test_types.py +0 -0
  266. {reader-3.24 → reader-3.26}/tests/utils.py +0 -0
  267. {reader-3.24 → reader-3.26}/tox.ini +0 -0
@@ -6,6 +6,42 @@ 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
+
21
+ Version 3.25
22
+ ------------
23
+
24
+ Released 2026-05-25
25
+
26
+ * Stable feed autodiscovery support. (:issue:`404`)
27
+
28
+ * Add :mod:`~reader.plugins.autodiscover` built-in plugin.
29
+ * Add :mod:`reader.discover` module providing low level discovery.
30
+ * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
31
+ when adding a feed; :ref:`screenshots <screenshots-autodiscovery>`.
32
+
33
+ * Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
34
+
35
+ * Don't fail for ``John Doe (...) (email)`` RSS authors.
36
+ * Support missing :attr:`EntrySource` ``author`` field during migration.
37
+
38
+ Note there should be no integrity issues,
39
+ as migrations happen inside a transaction.
40
+
41
+ * Simplify parser lazy initialization (make it explicit and opt-in by plugins).
42
+ * Allow before/after update hook functions to take *fewer* arguments than documented.
43
+
44
+
9
45
  Version 3.24
10
46
  ------------
11
47
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.24
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"
@@ -114,6 +116,7 @@ you'd probably call it a fat model.
114
116
  * full-text search articles
115
117
  * get statistics on feed and user activity
116
118
  * import / export feeds as OPML
119
+ * automatically discover feeds in web pages
117
120
  * write plugins to extend its functionality
118
121
  * skip all the low level stuff and focus on what makes your feed reader different
119
122
 
@@ -50,6 +50,7 @@ you'd probably call it a fat model.
50
50
  * full-text search articles
51
51
  * get statistics on feed and user activity
52
52
  * import / export feeds as OPML
53
+ * automatically discover feeds in web pages
53
54
  * write plugins to extend its functionality
54
55
  * skip all the low level stuff and focus on what makes your feed reader different
55
56
 
@@ -243,3 +243,9 @@ Utilities
243
243
  ---------------
244
244
 
245
245
  .. automodule:: reader.opml
246
+
247
+
248
+ ``reader.discover``
249
+ -------------------
250
+
251
+ .. automodule:: reader.discover
@@ -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
  -------------------
@@ -23,7 +23,7 @@ for name in [
23
23
  'structlog.contextvars',
24
24
  'structlog.stdlib',
25
25
  ]:
26
- sys.modules[name] = unittest.mock.Mock()
26
+ sys.modules[name] = unittest.mock.MagicMock()
27
27
 
28
28
  import reader
29
29
 
@@ -139,15 +139,18 @@ since each connection would be to a *different* database::
139
139
 
140
140
  .. _backups:
141
141
 
142
- Back-ups
143
- ~~~~~~~~
142
+ Backups
143
+ ~~~~~~~
144
144
 
145
- Making back-ups depends on the storage used.
145
+ Making backups generally depends on the storage used.
146
146
 
147
- For the SQLite storage, you should use
148
- the `sqlite3 .backup command <https://sqlite.org/cli.html>`_
149
- or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_
150
- (see :gh:`backup.sh <scripts/backup.sh>` for an example).
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
 
@@ -13,13 +13,10 @@ Dependencies
13
13
 
14
14
  These packages will be installed automatically when installing *reader*:
15
15
 
16
- * `feedparser`_ parses feeds; *reader* is essentially feedparser + state.
17
- * `requests`_ retrieves feeds from the internet;
18
- it replaces feedparser's default use of :mod:`urllib`
19
- to make it easier to write plugins.
16
+ * `feedparser`_ parses Atom and RSSfeeds.
17
+ * `requests`_ retrieves feeds from the internet.
20
18
  * `werkzeug`_ provides HTTP utilities.
21
- * `beautifulsoup4`_ is used to strip HTML tags before adding entries
22
- to the search index.
19
+ * `beautifulsoup4`_ is used to strip HTML tags and by various plugins.
23
20
  * `structlog`_ is used for structured logging.
24
21
  * `typing-extensions`_ is used for :mod:`typing` backports.
25
22
 
@@ -58,8 +55,13 @@ As of version |version|, *reader* has the following extras:
58
55
  * Specific plugins may require additional dependencies;
59
56
  refer to their documentation for details.
60
57
 
58
+ Beautiful Soup supports `multiple parsers`_,
59
+ and will use the best available one;
60
+ for speed, install `lxml`_.
61
61
 
62
62
  .. _beautifulsoup4: https://www.crummy.com/software/BeautifulSoup/
63
+ .. _multiple parsers: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser
64
+ .. _lxml: https://lxml.de/
63
65
  .. _feedparser: https://feedparser.readthedocs.io/en/latest/
64
66
  .. _requests: https://requests.readthedocs.io/
65
67
  .. _werkzeug: https://werkzeug.palletsprojects.com/
@@ -31,16 +31,6 @@ Parser
31
31
  :special-members: __call__
32
32
 
33
33
 
34
- .. module:: reader._parser.requests
35
-
36
- .. autoclass:: SessionFactory(...)
37
- :members:
38
- :special-members: __call__
39
-
40
- .. autoclass:: SessionWrapper(...)
41
- :members:
42
-
43
-
44
34
  Protocols
45
35
  ~~~~~~~~~
46
36
 
@@ -79,18 +69,6 @@ Protocols
79
69
  :show-inheritance:
80
70
 
81
71
 
82
- .. module:: reader._parser.requests
83
- :no-index:
84
-
85
- .. autoclass:: RequestHook
86
- :members:
87
- :special-members: __call__
88
-
89
- .. autoclass:: ResponseHook
90
- :members:
91
- :special-members: __call__
92
-
93
-
94
72
  Data objects
95
73
  ~~~~~~~~~~~~
96
74
 
@@ -113,6 +91,35 @@ Data objects
113
91
  :members:
114
92
 
115
93
 
94
+ Built-in retrievers and parsers
95
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
96
+
97
+ .. module:: reader._parser.http
98
+
99
+ .. autoclass:: HTTPRetriever
100
+ :members: request_hooks, response_hooks
101
+
102
+ .. autoclass:: RequestHook
103
+ :members:
104
+ :special-members: __call__
105
+
106
+ .. autoclass:: ResponseHook
107
+ :members:
108
+ :special-members: __call__
109
+
110
+ .. module:: reader._parser.file
111
+
112
+ .. autoclass:: FileRetriever
113
+
114
+ .. module:: reader._parser.feedparser
115
+
116
+ .. autoclass:: FeedparserParser
117
+
118
+ .. module:: reader._parser.jsonfeed
119
+
120
+ .. autoclass:: JSONFeedParser
121
+
122
+
116
123
  .. _storage:
117
124
 
118
125
  Storage
@@ -16,6 +16,7 @@ This is a list of built-in plugins that are considered stable.
16
16
  See the :ref:`Plugins <plugins>` section of the user guide
17
17
  for details on how built-in plugins are loaded.
18
18
 
19
+ .. automodule:: reader.plugins.autodiscover
19
20
  .. automodule:: reader.plugins.enclosure_dedupe
20
21
  .. automodule:: reader.plugins.entry_dedupe
21
22
  .. automodule:: reader.plugins.mark_as_read
@@ -65,8 +65,11 @@ cli = [
65
65
  app = [
66
66
  "flask>=0.10",
67
67
  "humanize>=4",
68
- # for config
68
+ # for legacy (tags)
69
69
  "PyYAML",
70
+ # for cli
71
+ "click",
72
+ "platformdirs",
70
73
  # for v2
71
74
  "WTForms",
72
75
  "Flask-WTF",
@@ -186,6 +189,7 @@ module = [
186
189
  "reader.plugins.ua_fallback",
187
190
  "reader.plugins.readtime",
188
191
  "reader.plugins.mark_as_read",
192
+ "reader.plugins.autodiscover",
189
193
  "reader._vendor.feedparser.*",
190
194
  ]
191
195
  ignore_errors = true
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.24'
40
+ __version__ = '3.26'
41
41
 
42
42
  # isort: off
43
43
 
@@ -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
@@ -88,6 +92,15 @@ def entries():
88
92
  if feed_url := form.feed.data:
89
93
  feed = reader.get_feed(feed_url)
90
94
 
95
+ autodiscover = None
96
+ if feed and feed.last_exception and not feed.last_updated:
97
+ autodiscover_tag = reader.make_reader_reserved_name('autodiscover')
98
+ hostname = urlparse(feed.url).hostname
99
+ autodiscover = [
100
+ (link, hostname and urlparse(link['href']).hostname == hostname)
101
+ for link in reader.get_tag(feed, autodiscover_tag, ())
102
+ ]
103
+
91
104
  kwargs = dict(form.data)
92
105
 
93
106
  if not (feed or kwargs.get('starting_after')):
@@ -106,6 +119,7 @@ def entries():
106
119
  form=form,
107
120
  entries=entries,
108
121
  feed=feed,
122
+ autodiscover=autodiscover,
109
123
  limit=limit,
110
124
  )
111
125
 
@@ -277,6 +291,31 @@ def add_feed():
277
291
  return render_template('add_feed.html', form=form)
278
292
 
279
293
 
294
+ @blueprint.route('/feeds/change', methods=['POST'])
295
+ def change_feed_url():
296
+ # NOTE: currently only for post-add autodiscover change,
297
+ # actual "change feed URL" UX needs design (e.g. needs preview)
298
+
299
+ reader = get_reader()
300
+ old = request.form['old']
301
+ new = request.form['new']
302
+
303
+ try:
304
+ reader.change_feed_url(old, new)
305
+ except FeedExistsError:
306
+ flash(f"Feed {new} already exists.", 'secondary')
307
+ return redirect(url_for('.entries', feed=old), code=303)
308
+ else:
309
+ # TODO: updating should be out of band
310
+ try:
311
+ reader.update_feed(new)
312
+ except UpdateError:
313
+ pass
314
+ else:
315
+ flash("Changed and updated feed.", 'success')
316
+ return redirect(url_for('.entries', feed=new), code=303)
317
+
318
+
280
319
  @blueprint.route('/feeds/import', methods=['GET', 'POST'])
281
320
  def import_feeds():
282
321
  reader = get_reader()
@@ -321,6 +360,38 @@ def help():
321
360
  return render_template('help.html')
322
361
 
323
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
+
324
395
  def stream_template(template_name_or_list, **kwargs):
325
396
  # Ensure flashed messages get removed from the session,
326
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
- _plugin_loader = PluginLoader('init_app')
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(config[''])
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
- :target {
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
  }
@@ -13,7 +13,15 @@
13
13
 
14
14
  <h1>Add feed</h1>
15
15
 
16
- <p>Add a single feed:</p>
16
+ <p>
17
+ Paste a feed or website URL:
18
+ <a
19
+ href="{{ url_for('.help', _anchor=help_topic) }}"
20
+ class="link-secondary d-inline-block px-2"
21
+ >
22
+ <i class="bi bi-question-lg" aria-label="help: adding feeds"></i>
23
+ </a>
24
+ </p>
17
25
 
18
26
  <form action="{{ url_for(request.endpoint) }}" method="post" class="my-2 mb-4">
19
27
  {#- TODO: aria stuff #}