reader 3.24__tar.gz → 3.25__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 (262) hide show
  1. {reader-3.24 → reader-3.25}/CHANGES.rst +23 -0
  2. {reader-3.24/src/reader.egg-info → reader-3.25}/PKG-INFO +2 -1
  3. {reader-3.24 → reader-3.25}/README.rst +1 -0
  4. {reader-3.24 → reader-3.25}/docs/api.rst +6 -0
  5. {reader-3.24 → reader-3.25}/docs/conf.py +1 -1
  6. {reader-3.24 → reader-3.25}/docs/install.rst +8 -6
  7. {reader-3.24 → reader-3.25}/docs/internal.rst +29 -22
  8. {reader-3.24 → reader-3.25}/docs/plugins.rst +1 -0
  9. {reader-3.24 → reader-3.25}/pyproject.toml +1 -0
  10. {reader-3.24 → reader-3.25}/src/reader/__init__.py +1 -1
  11. {reader-3.24 → reader-3.25}/src/reader/_app/__init__.py +35 -0
  12. {reader-3.24 → reader-3.25}/src/reader/_app/templates/add_feed.html +9 -1
  13. {reader-3.24 → reader-3.25}/src/reader/_app/templates/entries.html +58 -17
  14. {reader-3.24 → reader-3.25}/src/reader/_app/templates/entry.html +1 -2
  15. {reader-3.24 → reader-3.25}/src/reader/_app/templates/help.html +8 -2
  16. {reader-3.24 → reader-3.25}/src/reader/_app/templates/layout_base.html +1 -1
  17. {reader-3.24 → reader-3.25}/src/reader/_app/templates/macros.html +8 -0
  18. reader-3.24/src/reader/_parser/_lazy.py → reader-3.25/src/reader/_parser/__init__.py +481 -26
  19. {reader-3.24 → reader-3.25}/src/reader/_parser/feedparser.py +8 -2
  20. reader-3.25/src/reader/_parser/http.py +299 -0
  21. {reader-3.24 → reader-3.25}/src/reader/_parser/jsonfeed.py +6 -2
  22. {reader-3.24 → reader-3.25}/src/reader/_plugins/legacy/preview_feed_list.py +13 -8
  23. {reader-3.24 → reader-3.25}/src/reader/_plugins/sqlite_releases.py +4 -2
  24. {reader-3.24 → reader-3.25}/src/reader/_storage/_entries.py +1 -0
  25. {reader-3.24 → reader-3.25}/src/reader/_storage/_html_utils.py +9 -4
  26. {reader-3.24 → reader-3.25}/src/reader/_storage/_schema.py +1 -1
  27. {reader-3.24 → reader-3.25}/src/reader/_update/base.py +14 -13
  28. {reader-3.24 → reader-3.25}/src/reader/_update/hooks.py +34 -20
  29. {reader-3.24 → reader-3.25}/src/reader/_utils.py +0 -15
  30. {reader-3.24 → reader-3.25}/src/reader/core.py +20 -16
  31. reader-3.25/src/reader/discover.py +143 -0
  32. reader-3.25/src/reader/plugins/autodiscover.py +91 -0
  33. {reader-3.24 → reader-3.25}/src/reader/plugins/ua_fallback.py +4 -2
  34. {reader-3.24 → reader-3.25/src/reader.egg-info}/PKG-INFO +2 -1
  35. {reader-3.24 → reader-3.25}/src/reader.egg-info/SOURCES.txt +4 -3
  36. {reader-3.24 → reader-3.25}/tests/fakeparser.py +1 -2
  37. {reader-3.24 → reader-3.25}/tests/test_cli.py +4 -1
  38. reader-3.25/tests/test_discover.py +48 -0
  39. {reader-3.24 → reader-3.25}/tests/test_html_utils.py +1 -1
  40. {reader-3.24 → reader-3.25}/tests/test_lazy_imports.py +1 -1
  41. {reader-3.24 → reader-3.25}/tests/test_parser.py +34 -17
  42. reader-3.25/tests/test_plugins_autodiscover.py +69 -0
  43. {reader-3.24 → reader-3.25}/tests/test_reader.py +2 -2
  44. {reader-3.24 → reader-3.25}/tests/test_reader_hooks.py +18 -1
  45. {reader-3.24 → reader-3.25}/tests/test_storage.py +17 -8
  46. reader-3.24/src/reader/_parser/__init__.py +0 -510
  47. reader-3.24/src/reader/_parser/http.py +0 -107
  48. reader-3.24/src/reader/_parser/requests/__init__.py +0 -176
  49. reader-3.24/src/reader/_parser/requests/_lazy.py +0 -162
  50. {reader-3.24 → reader-3.25}/LICENSE +0 -0
  51. {reader-3.24 → reader-3.25}/MANIFEST.in +0 -0
  52. {reader-3.24 → reader-3.25}/docs/app.rst +0 -0
  53. {reader-3.24 → reader-3.25}/docs/changelog.rst +0 -0
  54. {reader-3.24 → reader-3.25}/docs/cli.rst +0 -0
  55. {reader-3.24 → reader-3.25}/docs/config.rst +0 -0
  56. {reader-3.24 → reader-3.25}/docs/contributing.rst +0 -0
  57. {reader-3.24 → reader-3.25}/docs/dev.rst +0 -0
  58. {reader-3.24 → reader-3.25}/docs/guide.rst +0 -0
  59. {reader-3.24 → reader-3.25}/docs/index.rst +0 -0
  60. {reader-3.24 → reader-3.25}/docs/screenshots/empty.png +0 -0
  61. {reader-3.24 → reader-3.25}/docs/screenshots/entries-filters-light.png +0 -0
  62. {reader-3.24 → reader-3.25}/docs/screenshots/entries.png +0 -0
  63. {reader-3.24 → reader-3.25}/docs/screenshots/entry-image-light.png +0 -0
  64. {reader-3.24 → reader-3.25}/docs/screenshots/entry.png +0 -0
  65. {reader-3.24 → reader-3.25}/docs/screenshots/feed.png +0 -0
  66. {reader-3.24 → reader-3.25}/docs/screenshots/feeds.png +0 -0
  67. {reader-3.24 → reader-3.25}/docs/screenshots/help.png +0 -0
  68. {reader-3.24 → reader-3.25}/docs/screenshots/import-result.png +0 -0
  69. {reader-3.24 → reader-3.25}/docs/screenshots/import-select.png +0 -0
  70. {reader-3.24 → reader-3.25}/docs/tutorial.rst +0 -0
  71. {reader-3.24 → reader-3.25}/docs/why.rst +0 -0
  72. {reader-3.24 → reader-3.25}/examples/config.toml +0 -0
  73. {reader-3.24 → reader-3.25}/examples/custom_headers.py +0 -0
  74. {reader-3.24 → reader-3.25}/examples/feed_slugs.py +0 -0
  75. {reader-3.24 → reader-3.25}/examples/parser_only.py +0 -0
  76. {reader-3.24 → reader-3.25}/examples/podcast.py +0 -0
  77. {reader-3.24 → reader-3.25}/examples/terminal.py +0 -0
  78. {reader-3.24 → reader-3.25}/run.sh +0 -0
  79. {reader-3.24 → reader-3.25}/scripts/backup.sh +0 -0
  80. {reader-3.24 → reader-3.25}/scripts/bench.py +0 -0
  81. {reader-3.24 → reader-3.25}/scripts/debug_storage_stats.py +0 -0
  82. {reader-3.24 → reader-3.25}/scripts/entry_dedupe_backtest.py +0 -0
  83. {reader-3.24 → reader-3.25}/scripts/jscontrols.html +0 -0
  84. {reader-3.24 → reader-3.25}/scripts/jscontrols.py +0 -0
  85. {reader-3.24 → reader-3.25}/scripts/lines.sh +0 -0
  86. {reader-3.24 → reader-3.25}/scripts/release.py +0 -0
  87. {reader-3.24 → reader-3.25}/scripts/vendor_app_stuff.py +0 -0
  88. {reader-3.24 → reader-3.25}/setup.cfg +0 -0
  89. {reader-3.24 → reader-3.25}/src/reader/__main__.py +0 -0
  90. {reader-3.24 → reader-3.25}/src/reader/_app/cli.py +0 -0
  91. {reader-3.24 → reader-3.25}/src/reader/_app/ext.py +0 -0
  92. {reader-3.24 → reader-3.25}/src/reader/_app/forms.py +0 -0
  93. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/__init__.py +0 -0
  94. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/api_thing.py +0 -0
  95. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/static/controls.js +0 -0
  96. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/static/style.css +0 -0
  97. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  98. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/entries.html +0 -0
  99. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/entry.html +0 -0
  100. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/feeds.html +0 -0
  101. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/layout.html +0 -0
  102. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/macros.html +0 -0
  103. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/metadata.html +0 -0
  104. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/templates/tags.html +0 -0
  105. {reader-3.24 → reader-3.25}/src/reader/_app/legacy/wsgi.py +0 -0
  106. {reader-3.24 → reader-3.25}/src/reader/_app/static/style.css +0 -0
  107. {reader-3.24 → reader-3.25}/src/reader/_app/static/theme.js +0 -0
  108. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/README +0 -0
  109. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  110. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  111. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  112. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  113. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  114. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  115. {reader-3.24 → reader-3.25}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  116. {reader-3.24 → reader-3.25}/src/reader/_app/templates/change_feed_title.html +0 -0
  117. {reader-3.24 → reader-3.25}/src/reader/_app/templates/delete_feed.html +0 -0
  118. {reader-3.24 → reader-3.25}/src/reader/_app/templates/feeds.html +0 -0
  119. {reader-3.24 → reader-3.25}/src/reader/_app/templates/import_feeds.html +0 -0
  120. {reader-3.24 → reader-3.25}/src/reader/_app/templates/layout.html +0 -0
  121. {reader-3.24 → reader-3.25}/src/reader/_app/wsgi.py +0 -0
  122. {reader-3.24 → reader-3.25}/src/reader/_cli.py +0 -0
  123. {reader-3.24 → reader-3.25}/src/reader/_config_utils.py +0 -0
  124. {reader-3.24 → reader-3.25}/src/reader/_hash_utils.py +0 -0
  125. {reader-3.24 → reader-3.25}/src/reader/_logging.py +0 -0
  126. {reader-3.24 → reader-3.25}/src/reader/_parser/_http_utils.py +0 -0
  127. {reader-3.24 → reader-3.25}/src/reader/_parser/_url_utils.py +0 -0
  128. {reader-3.24 → reader-3.25}/src/reader/_parser/file.py +0 -0
  129. {reader-3.24 → reader-3.25}/src/reader/_plugins/__init__.py +0 -0
  130. {reader-3.24 → reader-3.25}/src/reader/_plugins/cli_status.py +0 -0
  131. {reader-3.24 → reader-3.25}/src/reader/_plugins/legacy/__init__.py +0 -0
  132. {reader-3.24 → reader-3.25}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
  133. {reader-3.24 → reader-3.25}/src/reader/_plugins/legacy/share.py +0 -0
  134. {reader-3.24 → reader-3.25}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  135. {reader-3.24 → reader-3.25}/src/reader/_plugins/timer.py +0 -0
  136. {reader-3.24 → reader-3.25}/src/reader/_storage/__init__.py +0 -0
  137. {reader-3.24 → reader-3.25}/src/reader/_storage/_base.py +0 -0
  138. {reader-3.24 → reader-3.25}/src/reader/_storage/_changes.py +0 -0
  139. {reader-3.24 → reader-3.25}/src/reader/_storage/_feeds.py +0 -0
  140. {reader-3.24 → reader-3.25}/src/reader/_storage/_search.py +0 -0
  141. {reader-3.24 → reader-3.25}/src/reader/_storage/_sql_utils.py +0 -0
  142. {reader-3.24 → reader-3.25}/src/reader/_storage/_sqlite_utils.py +0 -0
  143. {reader-3.24 → reader-3.25}/src/reader/_storage/_tags.py +0 -0
  144. {reader-3.24 → reader-3.25}/src/reader/_types.py +0 -0
  145. {reader-3.24 → reader-3.25}/src/reader/_update/__init__.py +0 -0
  146. {reader-3.24 → reader-3.25}/src/reader/_vendor/__init__.py +0 -0
  147. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/__init__.py +0 -0
  148. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/api.py +0 -0
  149. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  150. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  151. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  152. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  153. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  154. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  155. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  156. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  157. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  158. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/encodings.py +0 -0
  159. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  160. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/html.py +0 -0
  161. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/http.py +0 -0
  162. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/mixin.py +0 -0
  163. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  164. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  165. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  166. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  167. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  168. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  169. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  170. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  171. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  172. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  173. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  174. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  175. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  176. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/py.typed +0 -0
  177. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  178. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/sgml.py +0 -0
  179. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/urls.py +0 -0
  180. {reader-3.24 → reader-3.25}/src/reader/_vendor/feedparser/util.py +0 -0
  181. {reader-3.24 → reader-3.25}/src/reader/exceptions.py +0 -0
  182. {reader-3.24 → reader-3.25}/src/reader/opml.py +0 -0
  183. {reader-3.24 → reader-3.25}/src/reader/plugins/__init__.py +0 -0
  184. {reader-3.24 → reader-3.25}/src/reader/plugins/_loader.py +0 -0
  185. {reader-3.24 → reader-3.25}/src/reader/plugins/enclosure_dedupe.py +0 -0
  186. {reader-3.24 → reader-3.25}/src/reader/plugins/entry_dedupe.py +0 -0
  187. {reader-3.24 → reader-3.25}/src/reader/plugins/mark_as_read.py +0 -0
  188. {reader-3.24 → reader-3.25}/src/reader/plugins/readtime.py +0 -0
  189. {reader-3.24 → reader-3.25}/src/reader/py.typed +0 -0
  190. {reader-3.24 → reader-3.25}/src/reader/types.py +0 -0
  191. {reader-3.24 → reader-3.25}/src/reader/utils.py +0 -0
  192. {reader-3.24 → reader-3.25}/src/reader.egg-info/dependency_links.txt +0 -0
  193. {reader-3.24 → reader-3.25}/src/reader.egg-info/requires.txt +0 -0
  194. {reader-3.24 → reader-3.25}/src/reader.egg-info/top_level.txt +0 -0
  195. {reader-3.24 → reader-3.25}/tests/_plugins/__init__.py +0 -0
  196. {reader-3.24 → reader-3.25}/tests/_plugins/builtin.py +0 -0
  197. {reader-3.24 → reader-3.25}/tests/_plugins/error_during_import.py +0 -0
  198. {reader-3.24 → reader-3.25}/tests/_plugins/error_import_error.py +0 -0
  199. {reader-3.24 → reader-3.25}/tests/conftest.py +0 -0
  200. {reader-3.24 → reader-3.25}/tests/data/10.json +0 -0
  201. {reader-3.24 → reader-3.25}/tests/data/10.json.py +0 -0
  202. {reader-3.24 → reader-3.25}/tests/data/custom +0 -0
  203. {reader-3.24 → reader-3.25}/tests/data/empty.atom +0 -0
  204. {reader-3.24 → reader-3.25}/tests/data/empty.atom.py +0 -0
  205. {reader-3.24 → reader-3.25}/tests/data/empty.json +0 -0
  206. {reader-3.24 → reader-3.25}/tests/data/empty.json.py +0 -0
  207. {reader-3.24 → reader-3.25}/tests/data/empty.rss +0 -0
  208. {reader-3.24 → reader-3.25}/tests/data/empty.rss.py +0 -0
  209. {reader-3.24 → reader-3.25}/tests/data/full.atom +0 -0
  210. {reader-3.24 → reader-3.25}/tests/data/full.atom.py +0 -0
  211. {reader-3.24 → reader-3.25}/tests/data/full.json +0 -0
  212. {reader-3.24 → reader-3.25}/tests/data/full.json.py +0 -0
  213. {reader-3.24 → reader-3.25}/tests/data/full.rss +0 -0
  214. {reader-3.24 → reader-3.25}/tests/data/full.rss.py +0 -0
  215. {reader-3.24 → reader-3.25}/tests/data/invalid.json +0 -0
  216. {reader-3.24 → reader-3.25}/tests/data/invalid.json.py +0 -0
  217. {reader-3.24 → reader-3.25}/tests/data/relative.atom +0 -0
  218. {reader-3.24 → reader-3.25}/tests/data/relative.atom.py +0 -0
  219. {reader-3.24 → reader-3.25}/tests/data/relative.rss +0 -0
  220. {reader-3.24 → reader-3.25}/tests/data/relative.rss.py +0 -0
  221. {reader-3.24 → reader-3.25}/tests/data/sqlite_releases.html +0 -0
  222. {reader-3.24 → reader-3.25}/tests/data/unknown.json +0 -0
  223. {reader-3.24 → reader-3.25}/tests/data/unknown.json.py +0 -0
  224. {reader-3.24 → reader-3.25}/tests/fixtures.py +0 -0
  225. {reader-3.24 → reader-3.25}/tests/reader_methods.py +0 -0
  226. {reader-3.24 → reader-3.25}/tests/test__types.py +0 -0
  227. {reader-3.24 → reader-3.25}/tests/test__utils.py +0 -0
  228. {reader-3.24 → reader-3.25}/tests/test_app_legacy.py +0 -0
  229. {reader-3.24 → reader-3.25}/tests/test_app_wsgi.py +0 -0
  230. {reader-3.24 → reader-3.25}/tests/test_bench.py +0 -0
  231. {reader-3.24 → reader-3.25}/tests/test_changes.py +0 -0
  232. {reader-3.24 → reader-3.25}/tests/test_config_utils.py +0 -0
  233. {reader-3.24 → reader-3.25}/tests/test_exceptions.py +0 -0
  234. {reader-3.24 → reader-3.25}/tests/test_hash_utils.py +0 -0
  235. {reader-3.24 → reader-3.25}/tests/test_logging.py +0 -0
  236. {reader-3.24 → reader-3.25}/tests/test_opml.py +0 -0
  237. {reader-3.24 → reader-3.25}/tests/test_plugins_cli_status.py +0 -0
  238. {reader-3.24 → reader-3.25}/tests/test_plugins_enclosure_dedupe.py +0 -0
  239. {reader-3.24 → reader-3.25}/tests/test_plugins_entry_dedupe.py +0 -0
  240. {reader-3.24 → reader-3.25}/tests/test_plugins_loader.py +0 -0
  241. {reader-3.24 → reader-3.25}/tests/test_plugins_mark_as_read.py +0 -0
  242. {reader-3.24 → reader-3.25}/tests/test_plugins_preview_feed_list.py +0 -0
  243. {reader-3.24 → reader-3.25}/tests/test_plugins_readtime.py +0 -0
  244. {reader-3.24 → reader-3.25}/tests/test_plugins_sqlite_releases.py +0 -0
  245. {reader-3.24 → reader-3.25}/tests/test_plugins_ua_fallback.py +0 -0
  246. {reader-3.24 → reader-3.25}/tests/test_reader_counts.py +0 -0
  247. {reader-3.24 → reader-3.25}/tests/test_reader_deprecations.py +0 -0
  248. {reader-3.24 → reader-3.25}/tests/test_reader_filter.py +0 -0
  249. {reader-3.24 → reader-3.25}/tests/test_reader_integration.py +0 -0
  250. {reader-3.24 → reader-3.25}/tests/test_reader_lifecycle.py +0 -0
  251. {reader-3.24 → reader-3.25}/tests/test_reader_private.py +0 -0
  252. {reader-3.24 → reader-3.25}/tests/test_reader_search.py +0 -0
  253. {reader-3.24 → reader-3.25}/tests/test_reader_sort.py +0 -0
  254. {reader-3.24 → reader-3.25}/tests/test_reader_update.py +0 -0
  255. {reader-3.24 → reader-3.25}/tests/test_reader_utils.py +0 -0
  256. {reader-3.24 → reader-3.25}/tests/test_search.py +0 -0
  257. {reader-3.24 → reader-3.25}/tests/test_sql_utils.py +0 -0
  258. {reader-3.24 → reader-3.25}/tests/test_sqlite_utils.py +0 -0
  259. {reader-3.24 → reader-3.25}/tests/test_tags.py +0 -0
  260. {reader-3.24 → reader-3.25}/tests/test_types.py +0 -0
  261. {reader-3.24 → reader-3.25}/tests/utils.py +0 -0
  262. {reader-3.24 → reader-3.25}/tox.ini +0 -0
@@ -6,6 +6,29 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.25
10
+ ------------
11
+
12
+ Released 2026-05-25
13
+
14
+ * Stable feed autodiscovery support. (:issue:`404`)
15
+
16
+ * Add :mod:`~reader.plugins.autodiscover` built-in plugin.
17
+ * Add :mod:`reader.discover` module providing low level discovery.
18
+ * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions when adding a feed.
19
+
20
+ * Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
21
+
22
+ * Don't fail for ``John Doe (...) (email)`` RSS authors.
23
+ * Support missing :attr:`EntrySource` ``author`` field during migration.
24
+
25
+ Note there should be no integrity issues,
26
+ as migrations happen inside a transaction.
27
+
28
+ * Simplify parser lazy initialization (make it explicit and opt-in by plugins).
29
+ * Allow before/after update hook functions to take *fewer* arguments than documented.
30
+
31
+
9
32
  Version 3.24
10
33
  ------------
11
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.24
3
+ Version: 3.25
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -114,6 +114,7 @@ you'd probably call it a fat model.
114
114
  * full-text search articles
115
115
  * get statistics on feed and user activity
116
116
  * import / export feeds as OPML
117
+ * automatically discover feeds in web pages
117
118
  * write plugins to extend its functionality
118
119
  * skip all the low level stuff and focus on what makes your feed reader different
119
120
 
@@ -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
@@ -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
 
@@ -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
@@ -186,6 +186,7 @@ module = [
186
186
  "reader.plugins.ua_fallback",
187
187
  "reader.plugins.readtime",
188
188
  "reader.plugins.mark_as_read",
189
+ "reader.plugins.autodiscover",
189
190
  "reader._vendor.feedparser.*",
190
191
  ]
191
192
  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.25'
41
41
 
42
42
  # isort: off
43
43
 
@@ -88,6 +88,15 @@ def entries():
88
88
  if feed_url := form.feed.data:
89
89
  feed = reader.get_feed(feed_url)
90
90
 
91
+ autodiscover = None
92
+ if feed and feed.last_exception and not feed.last_updated:
93
+ autodiscover_tag = reader.make_reader_reserved_name('autodiscover')
94
+ hostname = urlparse(feed.url).hostname
95
+ autodiscover = [
96
+ (link, hostname and urlparse(link['href']).hostname == hostname)
97
+ for link in reader.get_tag(feed, autodiscover_tag, None)
98
+ ]
99
+
91
100
  kwargs = dict(form.data)
92
101
 
93
102
  if not (feed or kwargs.get('starting_after')):
@@ -106,6 +115,7 @@ def entries():
106
115
  form=form,
107
116
  entries=entries,
108
117
  feed=feed,
118
+ autodiscover=autodiscover,
109
119
  limit=limit,
110
120
  )
111
121
 
@@ -277,6 +287,31 @@ def add_feed():
277
287
  return render_template('add_feed.html', form=form)
278
288
 
279
289
 
290
+ @blueprint.route('/feeds/change', methods=['POST'])
291
+ def change_feed_url():
292
+ # NOTE: currently only for post-add autodiscover change,
293
+ # actual "change feed URL" UX needs design (e.g. needs preview)
294
+
295
+ reader = get_reader()
296
+ old = request.form['old']
297
+ new = request.form['new']
298
+
299
+ try:
300
+ reader.change_feed_url(old, new)
301
+ except FeedExistsError:
302
+ flash(f"Feed {new} already exists.", 'secondary')
303
+ return redirect(url_for('.entries', feed=old), code=303)
304
+ else:
305
+ # TODO: updating should be out of band
306
+ try:
307
+ reader.update_feed(new)
308
+ except UpdateError:
309
+ pass
310
+ else:
311
+ flash("Changed and updated feed.", 'success')
312
+ return redirect(url_for('.entries', feed=new), code=303)
313
+
314
+
280
315
  @blueprint.route('/feeds/import', methods=['GET', 'POST'])
281
316
  def import_feeds():
282
317
  reader = get_reader()
@@ -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 #}
@@ -18,6 +18,64 @@
18
18
 
19
19
  {{ macros.flashed_messages() }}
20
20
 
21
+ {%- if feed and not feed.url.startswith('reader:') %}
22
+ <ul class="list-unstyled mt-3">
23
+
24
+ {%- if not feed.last_updated and not feed.last_exception %}
25
+ <li class="alert alert-secondary">
26
+ Feed not updated yet. <a href="">Refresh.</a>
27
+ </li>
28
+ {%- endif %}
29
+
30
+ {%- if feed.last_exception %}
31
+
32
+ {%- if autodiscover %}
33
+ <li class="alert alert-primary">
34
+
35
+ <p>
36
+ <strong>This doesn't look like a feed</strong>,
37
+ but it links to these feeds:
38
+ </p>
39
+
40
+ <ul class="list-unstyled d-table">
41
+ {%- for link, official in autodiscover | sort(attribute=1, reverse=True) %}
42
+ <li class="d-table-row mb-1">
43
+
44
+ <span class="d-table-cell pe-4 pb-1">
45
+ {{ macros.link_object(link) }}
46
+ {%- if not official %}
47
+ <i class="bi bi-box-arrow-up-right ms-1" aria-label="external feed" title="external feed"></i>
48
+ {%- endif %}
49
+ </span>
50
+
51
+ <form
52
+ action="{{ url_for('.change_feed_url') }}"
53
+ method="post"
54
+ class="d-table-cell pb-1"
55
+ >
56
+ {{ macros.csrf_token_field() }}
57
+ <input type="hidden" name="old" value="{{ feed.url }}">
58
+ <input type="hidden" name="new" value="{{ link.href }}">
59
+ <input type="submit" class="btn btn-primary btn-sm" value="add">
60
+ </form>
61
+
62
+ </li>
63
+ {%- endfor %}
64
+ </ul>
65
+
66
+ </li>
67
+ {%- endif %}
68
+
69
+ <li class="alert alert-{{ 'danger' if not autodiscover else 'secondary' }}">
70
+ <strong>Update error</strong>
71
+ {%- if not feed.last_updated %} (first update){% endif %}:
72
+ {{ feed.last_exception.value_str }}
73
+ </li>
74
+
75
+ {%- endif %}
76
+ </ul>
77
+ {%- endif %}
78
+
21
79
 
22
80
  {%- if not feed %}
23
81
  <h1>Entries</h1>
@@ -34,23 +92,6 @@
34
92
 
35
93
  {{ macros.subtitle(feed.subtitle) }}
36
94
 
37
- {%- if feed and not feed.url.startswith('reader:') %}
38
- <ul class="list-unstyled mt-3">
39
- {%- if not feed.last_updated and not feed.last_exception %}
40
- <li class="alert alert-secondary">
41
- Feed not updated yet. <a href="">Refresh.</a>
42
- </li>
43
- {%- endif %}
44
- {%- if feed.last_exception %}
45
- <li class="alert alert-danger">
46
- <strong>Update error</strong>
47
- {%- if not feed.last_updated %} (first update){% endif %}:
48
- {{ feed.last_exception.value_str }}
49
- </li>
50
- {%- endif %}
51
- </ul>
52
- {%- endif %}
53
-
54
95
  <form
55
96
  action="{{ url_for('.feed_actions', from='entries') }}"
56
97
  method="post"
@@ -114,8 +114,7 @@
114
114
  <ul class="card-text list-unstyled">
115
115
  {% for enclosure in entry.enclosures %}
116
116
  <li>
117
- <a href="{{ enclosure.href }}">{{ enclosure.href.split('/')[-1].split('?')[0] }}</a>
118
- <small>{{ enclosure.type }}</small>
117
+ {{ macros.link_object(enclosure) }}
119
118
  </li>
120
119
  {% endfor %}
121
120
  </ul>
@@ -39,11 +39,17 @@
39
39
  <h3 id="add-feed">Adding feeds</h3>
40
40
 
41
41
  <p>
42
- To add a feed, paste its link in the
42
+ To add a feed, paste the feed or website URL on the
43
43
  <a href="{{ url_for('.add_feed') }}">Add feed</a> page.
44
44
 
45
45
  <p>
46
- You can usually find if a website has a feed
46
+ If you use a website URL,
47
+ <em>reader</em> will discover any standard feeds it links to
48
+ and suggest them to you.
49
+
50
+ <p>
51
+ If that doesn't work,
52
+ you can usually find if a website has a feed
47
53
  by looking in its header or footer for this icon
48
54
  <i class="bi bi-rss" aria-label="RSS logo"></i>,
49
55
  or for links called <em>feed</em>, <em>RSS</em>, or <em>Atom</em>.
@@ -68,7 +68,7 @@
68
68
  class="btn btn-link nav-link d-flex align-items-center"
69
69
  >
70
70
  <i class="bi bi-question-lg"></i>
71
- <span class="d-md-none ms-2" id="theme-text">help</span>
71
+ <span class="d-md-none ms-2">help</span>
72
72
  </a>
73
73
  </li>
74
74
 
@@ -355,3 +355,11 @@
355
355
  or learn more about <a href="{{ url_for('.help') }}">how to use <strong>reader</strong></a>.
356
356
  </p>
357
357
  {%- endmacro -%}
358
+
359
+
360
+ {%- macro link_object(link) %}
361
+ {#- TODO: when links are unified, add a property for this #}
362
+ {%- set title = link.title or link.href.rpartition('?')[0].rstrip('/').rpartition('/')[2] %}
363
+ <a href="{{ link.href }}" target="_blank">{{ title }}</a>
364
+ {%- if link.type %}&ensp;<small>{{ link.type }}</small>{% endif %}
365
+ {%- endmacro %}