reader 3.23__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 (264) hide show
  1. {reader-3.23 → reader-3.25}/CHANGES.rst +69 -0
  2. {reader-3.23/src/reader.egg-info → reader-3.25}/PKG-INFO +4 -3
  3. {reader-3.23 → reader-3.25}/README.rst +1 -0
  4. {reader-3.23 → reader-3.25}/docs/api.rst +14 -0
  5. {reader-3.23 → reader-3.25}/docs/app.rst +11 -0
  6. {reader-3.23 → reader-3.25}/docs/conf.py +4 -1
  7. {reader-3.23 → reader-3.25}/docs/guide.rst +31 -2
  8. {reader-3.23 → reader-3.25}/docs/install.rst +10 -7
  9. {reader-3.23 → reader-3.25}/docs/internal.rst +29 -22
  10. {reader-3.23 → reader-3.25}/docs/plugins.rst +1 -0
  11. reader-3.25/docs/screenshots/empty.png +0 -0
  12. reader-3.25/docs/screenshots/help.png +0 -0
  13. reader-3.25/docs/screenshots/import-select.png +0 -0
  14. {reader-3.23 → reader-3.25}/pyproject.toml +8 -3
  15. {reader-3.23 → reader-3.25}/src/reader/__init__.py +4 -1
  16. {reader-3.23 → reader-3.25}/src/reader/_app/__init__.py +49 -0
  17. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/entry.html +9 -1
  18. {reader-3.23 → reader-3.25}/src/reader/_app/static/style.css +6 -0
  19. {reader-3.23 → reader-3.25}/src/reader/_app/templates/add_feed.html +10 -1
  20. {reader-3.23 → reader-3.25}/src/reader/_app/templates/change_feed_title.html +1 -0
  21. {reader-3.23 → reader-3.25}/src/reader/_app/templates/delete_feed.html +1 -0
  22. {reader-3.23 → reader-3.25}/src/reader/_app/templates/entries.html +74 -18
  23. {reader-3.23 → reader-3.25}/src/reader/_app/templates/entry.html +2 -2
  24. {reader-3.23 → reader-3.25}/src/reader/_app/templates/feeds.html +5 -1
  25. reader-3.25/src/reader/_app/templates/help.html +137 -0
  26. {reader-3.23 → reader-3.25}/src/reader/_app/templates/import_feeds.html +12 -2
  27. {reader-3.23 → reader-3.25}/src/reader/_app/templates/layout_base.html +11 -1
  28. {reader-3.23 → reader-3.25}/src/reader/_app/templates/macros.html +49 -3
  29. {reader-3.23 → reader-3.25}/src/reader/_cli.py +17 -6
  30. {reader-3.23 → reader-3.25}/src/reader/_config_utils.py +7 -3
  31. reader-3.25/src/reader/_logging.py +80 -0
  32. reader-3.25/src/reader/_parser/__init__.py +976 -0
  33. {reader-3.23 → reader-3.25}/src/reader/_parser/feedparser.py +100 -13
  34. reader-3.25/src/reader/_parser/http.py +299 -0
  35. {reader-3.23 → reader-3.25}/src/reader/_parser/jsonfeed.py +35 -33
  36. {reader-3.23 → reader-3.25}/src/reader/_plugins/legacy/enclosure_tags.py +1 -1
  37. {reader-3.23 → reader-3.25}/src/reader/_plugins/legacy/preview_feed_list.py +13 -8
  38. {reader-3.23 → reader-3.25}/src/reader/_plugins/sqlite_releases.py +4 -2
  39. {reader-3.23 → reader-3.25}/src/reader/_storage/_entries.py +56 -19
  40. {reader-3.23 → reader-3.25}/src/reader/_storage/_feeds.py +65 -39
  41. {reader-3.23 → reader-3.25}/src/reader/_storage/_html_utils.py +9 -4
  42. {reader-3.23 → reader-3.25}/src/reader/_storage/_schema.py +45 -1
  43. {reader-3.23 → reader-3.25}/src/reader/_storage/_search.py +3 -13
  44. {reader-3.23 → reader-3.25}/src/reader/_storage/_sqlite_utils.py +2 -0
  45. {reader-3.23 → reader-3.25}/src/reader/_types.py +15 -99
  46. reader-3.23/src/reader/_update.py → reader-3.25/src/reader/_update/__init__.py +95 -196
  47. reader-3.25/src/reader/_update/base.py +206 -0
  48. reader-3.25/src/reader/_update/hooks.py +119 -0
  49. {reader-3.23 → reader-3.25}/src/reader/_utils.py +25 -76
  50. {reader-3.23 → reader-3.25}/src/reader/core.py +26 -51
  51. reader-3.25/src/reader/discover.py +143 -0
  52. reader-3.25/src/reader/plugins/autodiscover.py +91 -0
  53. {reader-3.23 → reader-3.25}/src/reader/plugins/entry_dedupe.py +29 -40
  54. {reader-3.23 → reader-3.25}/src/reader/plugins/mark_as_read.py +5 -7
  55. {reader-3.23 → reader-3.25}/src/reader/plugins/readtime.py +8 -10
  56. {reader-3.23 → reader-3.25}/src/reader/plugins/ua_fallback.py +7 -12
  57. {reader-3.23 → reader-3.25}/src/reader/types.py +64 -9
  58. {reader-3.23 → reader-3.25/src/reader.egg-info}/PKG-INFO +4 -3
  59. {reader-3.23 → reader-3.25}/src/reader.egg-info/SOURCES.txt +13 -4
  60. {reader-3.23 → reader-3.25}/src/reader.egg-info/requires.txt +2 -2
  61. {reader-3.23 → reader-3.25}/tests/conftest.py +3 -39
  62. {reader-3.23 → reader-3.25}/tests/data/empty.atom.py +1 -0
  63. {reader-3.23 → reader-3.25}/tests/data/full.atom.py +4 -3
  64. {reader-3.23 → reader-3.25}/tests/data/full.json.py +7 -3
  65. {reader-3.23 → reader-3.25}/tests/data/full.rss.py +3 -2
  66. {reader-3.23 → reader-3.25}/tests/data/invalid.json.py +3 -2
  67. {reader-3.23 → reader-3.25}/tests/fakeparser.py +1 -2
  68. reader-3.25/tests/fixtures.py +48 -0
  69. {reader-3.23 → reader-3.25}/tests/test__types.py +14 -5
  70. {reader-3.23 → reader-3.25}/tests/test__utils.py +0 -19
  71. {reader-3.23 → reader-3.25}/tests/test_cli.py +22 -5
  72. {reader-3.23 → reader-3.25}/tests/test_config_utils.py +8 -0
  73. reader-3.25/tests/test_discover.py +48 -0
  74. {reader-3.23 → reader-3.25}/tests/test_html_utils.py +1 -1
  75. {reader-3.23 → reader-3.25}/tests/test_lazy_imports.py +1 -3
  76. reader-3.25/tests/test_logging.py +41 -0
  77. {reader-3.23 → reader-3.25}/tests/test_parser.py +235 -30
  78. reader-3.25/tests/test_plugins_autodiscover.py +69 -0
  79. {reader-3.23 → reader-3.25}/tests/test_plugins_ua_fallback.py +1 -1
  80. {reader-3.23 → reader-3.25}/tests/test_reader.py +18 -15
  81. {reader-3.23 → reader-3.25}/tests/test_reader_hooks.py +18 -1
  82. {reader-3.23 → reader-3.25}/tests/test_reader_search.py +7 -2
  83. {reader-3.23 → reader-3.25}/tests/test_reader_update.py +11 -9
  84. {reader-3.23 → reader-3.25}/tests/test_storage.py +37 -0
  85. {reader-3.23 → reader-3.25}/tests/test_types.py +33 -0
  86. reader-3.23/docs/screenshots/import-select.png +0 -0
  87. reader-3.23/src/reader/_parser/__init__.py +0 -502
  88. reader-3.23/src/reader/_parser/_lazy.py +0 -542
  89. reader-3.23/src/reader/_parser/http.py +0 -107
  90. reader-3.23/src/reader/_parser/requests/__init__.py +0 -176
  91. reader-3.23/src/reader/_parser/requests/_lazy.py +0 -162
  92. {reader-3.23 → reader-3.25}/LICENSE +0 -0
  93. {reader-3.23 → reader-3.25}/MANIFEST.in +0 -0
  94. {reader-3.23 → reader-3.25}/docs/changelog.rst +0 -0
  95. {reader-3.23 → reader-3.25}/docs/cli.rst +0 -0
  96. {reader-3.23 → reader-3.25}/docs/config.rst +0 -0
  97. {reader-3.23 → reader-3.25}/docs/contributing.rst +0 -0
  98. {reader-3.23 → reader-3.25}/docs/dev.rst +0 -0
  99. {reader-3.23 → reader-3.25}/docs/index.rst +0 -0
  100. {reader-3.23 → reader-3.25}/docs/screenshots/entries-filters-light.png +0 -0
  101. {reader-3.23 → reader-3.25}/docs/screenshots/entries.png +0 -0
  102. {reader-3.23 → reader-3.25}/docs/screenshots/entry-image-light.png +0 -0
  103. {reader-3.23 → reader-3.25}/docs/screenshots/entry.png +0 -0
  104. {reader-3.23 → reader-3.25}/docs/screenshots/feed.png +0 -0
  105. {reader-3.23 → reader-3.25}/docs/screenshots/feeds.png +0 -0
  106. {reader-3.23 → reader-3.25}/docs/screenshots/import-result.png +0 -0
  107. {reader-3.23 → reader-3.25}/docs/tutorial.rst +0 -0
  108. {reader-3.23 → reader-3.25}/docs/why.rst +0 -0
  109. {reader-3.23 → reader-3.25}/examples/config.toml +0 -0
  110. {reader-3.23 → reader-3.25}/examples/custom_headers.py +0 -0
  111. {reader-3.23 → reader-3.25}/examples/feed_slugs.py +0 -0
  112. {reader-3.23 → reader-3.25}/examples/parser_only.py +0 -0
  113. {reader-3.23 → reader-3.25}/examples/podcast.py +0 -0
  114. {reader-3.23 → reader-3.25}/examples/terminal.py +0 -0
  115. {reader-3.23 → reader-3.25}/run.sh +0 -0
  116. {reader-3.23 → reader-3.25}/scripts/backup.sh +0 -0
  117. {reader-3.23 → reader-3.25}/scripts/bench.py +0 -0
  118. {reader-3.23 → reader-3.25}/scripts/debug_storage_stats.py +0 -0
  119. {reader-3.23 → reader-3.25}/scripts/entry_dedupe_backtest.py +0 -0
  120. {reader-3.23 → reader-3.25}/scripts/jscontrols.html +0 -0
  121. {reader-3.23 → reader-3.25}/scripts/jscontrols.py +0 -0
  122. {reader-3.23 → reader-3.25}/scripts/lines.sh +0 -0
  123. {reader-3.23 → reader-3.25}/scripts/release.py +0 -0
  124. {reader-3.23 → reader-3.25}/scripts/vendor_app_stuff.py +0 -0
  125. {reader-3.23 → reader-3.25}/setup.cfg +0 -0
  126. {reader-3.23 → reader-3.25}/src/reader/__main__.py +0 -0
  127. {reader-3.23 → reader-3.25}/src/reader/_app/cli.py +0 -0
  128. {reader-3.23 → reader-3.25}/src/reader/_app/ext.py +0 -0
  129. {reader-3.23 → reader-3.25}/src/reader/_app/forms.py +0 -0
  130. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/__init__.py +0 -0
  131. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/api_thing.py +0 -0
  132. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/static/controls.js +0 -0
  133. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/static/style.css +0 -0
  134. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  135. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/entries.html +0 -0
  136. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/feeds.html +0 -0
  137. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/layout.html +0 -0
  138. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/macros.html +0 -0
  139. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/metadata.html +0 -0
  140. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/templates/tags.html +0 -0
  141. {reader-3.23 → reader-3.25}/src/reader/_app/legacy/wsgi.py +0 -0
  142. {reader-3.23 → reader-3.25}/src/reader/_app/static/theme.js +0 -0
  143. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/README +0 -0
  144. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  145. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  146. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  147. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  148. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  149. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  150. {reader-3.23 → reader-3.25}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  151. {reader-3.23 → reader-3.25}/src/reader/_app/templates/layout.html +0 -0
  152. {reader-3.23 → reader-3.25}/src/reader/_app/wsgi.py +0 -0
  153. {reader-3.23 → reader-3.25}/src/reader/_hash_utils.py +0 -0
  154. {reader-3.23 → reader-3.25}/src/reader/_parser/_http_utils.py +0 -0
  155. {reader-3.23 → reader-3.25}/src/reader/_parser/_url_utils.py +0 -0
  156. {reader-3.23 → reader-3.25}/src/reader/_parser/file.py +0 -0
  157. {reader-3.23 → reader-3.25}/src/reader/_plugins/__init__.py +0 -0
  158. {reader-3.23 → reader-3.25}/src/reader/_plugins/cli_status.py +0 -0
  159. {reader-3.23 → reader-3.25}/src/reader/_plugins/legacy/__init__.py +0 -0
  160. {reader-3.23 → reader-3.25}/src/reader/_plugins/legacy/share.py +0 -0
  161. {reader-3.23 → reader-3.25}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  162. {reader-3.23 → reader-3.25}/src/reader/_plugins/timer.py +0 -0
  163. {reader-3.23 → reader-3.25}/src/reader/_storage/__init__.py +0 -0
  164. {reader-3.23 → reader-3.25}/src/reader/_storage/_base.py +0 -0
  165. {reader-3.23 → reader-3.25}/src/reader/_storage/_changes.py +0 -0
  166. {reader-3.23 → reader-3.25}/src/reader/_storage/_sql_utils.py +0 -0
  167. {reader-3.23 → reader-3.25}/src/reader/_storage/_tags.py +0 -0
  168. {reader-3.23 → reader-3.25}/src/reader/_vendor/__init__.py +0 -0
  169. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/__init__.py +0 -0
  170. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/api.py +0 -0
  171. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  172. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  173. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  174. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  175. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  176. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  177. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  178. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  179. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  180. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/encodings.py +0 -0
  181. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  182. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/html.py +0 -0
  183. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/http.py +0 -0
  184. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/mixin.py +0 -0
  185. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  186. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  187. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  188. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  189. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  190. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  191. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  192. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  193. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  194. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  195. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  196. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  197. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  198. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/py.typed +0 -0
  199. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  200. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/sgml.py +0 -0
  201. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/urls.py +0 -0
  202. {reader-3.23 → reader-3.25}/src/reader/_vendor/feedparser/util.py +0 -0
  203. {reader-3.23 → reader-3.25}/src/reader/exceptions.py +0 -0
  204. {reader-3.23 → reader-3.25}/src/reader/opml.py +0 -0
  205. {reader-3.23 → reader-3.25}/src/reader/plugins/__init__.py +0 -0
  206. {reader-3.23 → reader-3.25}/src/reader/plugins/_loader.py +0 -0
  207. {reader-3.23 → reader-3.25}/src/reader/plugins/enclosure_dedupe.py +0 -0
  208. {reader-3.23 → reader-3.25}/src/reader/py.typed +0 -0
  209. {reader-3.23 → reader-3.25}/src/reader/utils.py +0 -0
  210. {reader-3.23 → reader-3.25}/src/reader.egg-info/dependency_links.txt +0 -0
  211. {reader-3.23 → reader-3.25}/src/reader.egg-info/top_level.txt +0 -0
  212. {reader-3.23 → reader-3.25}/tests/_plugins/__init__.py +0 -0
  213. {reader-3.23 → reader-3.25}/tests/_plugins/builtin.py +0 -0
  214. {reader-3.23 → reader-3.25}/tests/_plugins/error_during_import.py +0 -0
  215. {reader-3.23 → reader-3.25}/tests/_plugins/error_import_error.py +0 -0
  216. {reader-3.23 → reader-3.25}/tests/data/10.json +0 -0
  217. {reader-3.23 → reader-3.25}/tests/data/10.json.py +0 -0
  218. {reader-3.23 → reader-3.25}/tests/data/custom +0 -0
  219. {reader-3.23 → reader-3.25}/tests/data/empty.atom +0 -0
  220. {reader-3.23 → reader-3.25}/tests/data/empty.json +0 -0
  221. {reader-3.23 → reader-3.25}/tests/data/empty.json.py +0 -0
  222. {reader-3.23 → reader-3.25}/tests/data/empty.rss +0 -0
  223. {reader-3.23 → reader-3.25}/tests/data/empty.rss.py +0 -0
  224. {reader-3.23 → reader-3.25}/tests/data/full.atom +0 -0
  225. {reader-3.23 → reader-3.25}/tests/data/full.json +0 -0
  226. {reader-3.23 → reader-3.25}/tests/data/full.rss +0 -0
  227. {reader-3.23 → reader-3.25}/tests/data/invalid.json +0 -0
  228. {reader-3.23 → reader-3.25}/tests/data/relative.atom +0 -0
  229. {reader-3.23 → reader-3.25}/tests/data/relative.atom.py +0 -0
  230. {reader-3.23 → reader-3.25}/tests/data/relative.rss +0 -0
  231. {reader-3.23 → reader-3.25}/tests/data/relative.rss.py +0 -0
  232. {reader-3.23 → reader-3.25}/tests/data/sqlite_releases.html +0 -0
  233. {reader-3.23 → reader-3.25}/tests/data/unknown.json +0 -0
  234. {reader-3.23 → reader-3.25}/tests/data/unknown.json.py +0 -0
  235. {reader-3.23 → reader-3.25}/tests/reader_methods.py +0 -0
  236. {reader-3.23 → reader-3.25}/tests/test_app_legacy.py +0 -0
  237. {reader-3.23 → reader-3.25}/tests/test_app_wsgi.py +0 -0
  238. {reader-3.23 → reader-3.25}/tests/test_bench.py +0 -0
  239. {reader-3.23 → reader-3.25}/tests/test_changes.py +0 -0
  240. {reader-3.23 → reader-3.25}/tests/test_exceptions.py +0 -0
  241. {reader-3.23 → reader-3.25}/tests/test_hash_utils.py +0 -0
  242. {reader-3.23 → reader-3.25}/tests/test_opml.py +0 -0
  243. {reader-3.23 → reader-3.25}/tests/test_plugins_cli_status.py +0 -0
  244. {reader-3.23 → reader-3.25}/tests/test_plugins_enclosure_dedupe.py +0 -0
  245. {reader-3.23 → reader-3.25}/tests/test_plugins_entry_dedupe.py +0 -0
  246. {reader-3.23 → reader-3.25}/tests/test_plugins_loader.py +0 -0
  247. {reader-3.23 → reader-3.25}/tests/test_plugins_mark_as_read.py +0 -0
  248. {reader-3.23 → reader-3.25}/tests/test_plugins_preview_feed_list.py +0 -0
  249. {reader-3.23 → reader-3.25}/tests/test_plugins_readtime.py +0 -0
  250. {reader-3.23 → reader-3.25}/tests/test_plugins_sqlite_releases.py +0 -0
  251. {reader-3.23 → reader-3.25}/tests/test_reader_counts.py +0 -0
  252. {reader-3.23 → reader-3.25}/tests/test_reader_deprecations.py +0 -0
  253. {reader-3.23 → reader-3.25}/tests/test_reader_filter.py +0 -0
  254. {reader-3.23 → reader-3.25}/tests/test_reader_integration.py +0 -0
  255. {reader-3.23 → reader-3.25}/tests/test_reader_lifecycle.py +0 -0
  256. {reader-3.23 → reader-3.25}/tests/test_reader_private.py +0 -0
  257. {reader-3.23 → reader-3.25}/tests/test_reader_sort.py +0 -0
  258. {reader-3.23 → reader-3.25}/tests/test_reader_utils.py +0 -0
  259. {reader-3.23 → reader-3.25}/tests/test_search.py +0 -0
  260. {reader-3.23 → reader-3.25}/tests/test_sql_utils.py +0 -0
  261. {reader-3.23 → reader-3.25}/tests/test_sqlite_utils.py +0 -0
  262. {reader-3.23 → reader-3.25}/tests/test_tags.py +0 -0
  263. {reader-3.23 → reader-3.25}/tests/utils.py +0 -0
  264. {reader-3.23 → reader-3.25}/tox.ini +0 -0
@@ -6,6 +6,75 @@ 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
+
32
+ Version 3.24
33
+ ------------
34
+
35
+ Released 2026-05-20
36
+
37
+ * Support multiple authors with details (link, email).
38
+ Thanks to `Anshul Mittal`_ for the PR.
39
+ (:issue:`391`)
40
+
41
+ * Add :attr:`Feed.authors`, :attr:`Entry.authors`,
42
+ and :attr:`EntrySource.authors` attributes.
43
+ * Add matching :attr:`~Feed.authors_str` property.
44
+ * Deprecate :attr:`~Feed.author` attribute.
45
+
46
+ * Fix bug introduced in `version 3.15 <Version 3.15_>`__
47
+ causing feeds to not be retrieved in parallel
48
+ even if :meth:`~Reader.update_feeds` was called with ``workers`` > 1.
49
+
50
+ * Use :ref:`structured logging <logging>` internally,
51
+ with the possibility to opt into native `structlog`_ logging.
52
+ (:issue:`147`)
53
+
54
+ * Use :meth:`datetime.datetime.fromisoformat` instead of external dependency ``iso8601``.
55
+ Thanks to `Sebastian Bradley`_ for the PR.
56
+ (:issue:`398`)
57
+
58
+ * Refactor updater internals to allow reusing logic in alternate implementations.
59
+
60
+ * :doc:`app` improvements.
61
+
62
+ * Add empty state and contextual help; :ref:`screenshots <screenshots-help>`.
63
+ * Import feeds improvements (select all/none, accept only XML / OPML files).
64
+
65
+ * Add ``--json`` option to the ``list`` CLI commands.
66
+ Thanks to `Puneet Dixit`_ for the PR.
67
+ (:issue:`394`)
68
+
69
+
70
+
71
+ .. _Puneet Dixit: https://github.com/puneetdixit200
72
+ .. _Anshul Mittal: https://github.com/anderson688
73
+ .. _Sebastian Bradley: https://github.com/bazbrad765
74
+
75
+ .. _structlog: https://www.structlog.org/
76
+
77
+
9
78
  Version 3.23
10
79
  ------------
11
80
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.23
3
+ Version: 3.25
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -39,12 +39,12 @@ Requires-Dist: typing-extensions>=4
39
39
  Requires-Dist: feedparser>=6
40
40
  Requires-Dist: requests>=2.18
41
41
  Requires-Dist: werkzeug>2
42
- Requires-Dist: iso8601>=1
43
42
  Requires-Dist: beautifulsoup4>=4.5
43
+ Requires-Dist: structlog
44
44
  Provides-Extra: all
45
45
  Requires-Dist: reader[app,cli,unstable-plugins]; extra == "all"
46
46
  Provides-Extra: cli
47
- Requires-Dist: click>=7; extra == "cli"
47
+ Requires-Dist: click!=8.4.0,>=7; extra == "cli"
48
48
  Provides-Extra: app
49
49
  Requires-Dist: flask>=0.10; extra == "app"
50
50
  Requires-Dist: humanize>=4; extra == "app"
@@ -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
 
@@ -27,12 +27,17 @@ Data objects
27
27
 
28
28
  .. autoclass:: Feed
29
29
  :members:
30
+ :inherited-members:
31
+
32
+ .. autoclass:: Author
33
+ :members:
30
34
 
31
35
  .. autoclass:: ExceptionInfo
32
36
  :members:
33
37
 
34
38
  .. autoclass:: Entry
35
39
  :members:
40
+ :inherited-members:
36
41
 
37
42
  .. autoclass:: Content
38
43
  :members:
@@ -42,6 +47,7 @@ Data objects
42
47
 
43
48
  .. autoclass:: EntrySource
44
49
  :members:
50
+ :inherited-members:
45
51
 
46
52
  .. autoclass:: EntrySearchResult
47
53
  :members:
@@ -227,6 +233,8 @@ Constants
227
233
  Utilities
228
234
  ---------
229
235
 
236
+ .. autofunction:: reader.enable_structlog
237
+
230
238
  .. autofunction:: reader.utils.archive_entries
231
239
 
232
240
 
@@ -235,3 +243,9 @@ Utilities
235
243
  ---------------
236
244
 
237
245
  .. automodule:: reader.opml
246
+
247
+
248
+ ``reader.discover``
249
+ -------------------
250
+
251
+ .. automodule:: reader.discover
@@ -74,6 +74,17 @@ Screenshots
74
74
 
75
75
  import feeds (result)
76
76
 
77
+ .. _screenshots-help:
78
+
79
+ .. figure:: screenshots/empty.png
80
+ :width: 240px
81
+
82
+ new user / empty state
83
+
84
+ .. figure:: screenshots/help.png
85
+ :width: 240px
86
+
87
+ contextual help
77
88
 
78
89
 
79
90
  Serving the web app
@@ -19,8 +19,11 @@ for name in [
19
19
  'werkzeug.exceptions',
20
20
  'werkzeug.http',
21
21
  'yaml',
22
+ 'structlog',
23
+ 'structlog.contextvars',
24
+ 'structlog.stdlib',
22
25
  ]:
23
- sys.modules[name] = unittest.mock.Mock()
26
+ sys.modules[name] = unittest.mock.MagicMock()
24
27
 
25
28
  import reader
26
29
 
@@ -374,7 +374,7 @@ As seen in the previous sections,
374
374
  updated=datetime.datetime(2020, 2, 28, 9, 34, 2, tzinfo=datetime.timezone.utc),
375
375
  title='Hello Internet',
376
376
  link='http://www.hellointernet.fm/',
377
- author='CGP Grey',
377
+ authors=(Author(name='CGP Grey'),),
378
378
  subtitle='CGP Grey and Brady Haran talk about YouTube, life, work, whatever.',
379
379
  version='rss20',
380
380
  user_title=None,
@@ -386,9 +386,10 @@ As seen in the previous sections,
386
386
  To get all the feeds, use the :meth:`~Reader.get_feeds` method::
387
387
 
388
388
  >>> for feed in reader.get_feeds():
389
+ ... authors = ", ".join(a.name for a in feed.authors if a.name) or 'unknown author'
389
390
  ... print(
390
391
  ... feed.title or feed.url,
391
- ... f"by {feed.author or 'unknown author'},",
392
+ ... f"by {authors},",
392
393
  ... f"updated on {feed.updated or 'never'}",
393
394
  ... )
394
395
  ...
@@ -1001,6 +1002,34 @@ depending on their type attribute or feedparser defaults:
1001
1002
 
1002
1003
 
1003
1004
 
1005
+ .. _logging:
1006
+
1007
+ Logging
1008
+ -------
1009
+
1010
+ *reader* uses structured logging internally, through `structlog`_.
1011
+
1012
+ By default, output goes to the standard library :mod:`logging` module;
1013
+ if you don't care about structlog,
1014
+ configure :mod:`logging` as you normally would.
1015
+ The *reader* root logger is called ``reader``;
1016
+ the names of its children are an implementation detail,
1017
+ and may change without any notice.
1018
+
1019
+ To enable native structlog logging,
1020
+ call :func:`enable_structlog` **before** configuring structlog::
1021
+
1022
+ import reader, structlog
1023
+
1024
+ reader.enable_structlog()
1025
+ structlog.configure(...)
1026
+
1027
+ ... # do stuff with reader
1028
+
1029
+ .. _structlog: https://www.structlog.org/
1030
+
1031
+
1032
+
1004
1033
  .. _errors:
1005
1034
 
1006
1035
  Errors and exceptions
@@ -13,14 +13,11 @@ 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
- * `iso8601`_ parses dates in ISO 8601 / RFC 3339; used for JSON Feed parsing.
22
- * `beautifulsoup4`_ is used to strip HTML tags before adding entries
23
- to the search index.
19
+ * `beautifulsoup4`_ is used to strip HTML tags and by various plugins.
20
+ * `structlog`_ is used for structured logging.
24
21
  * `typing-extensions`_ is used for :mod:`typing` backports.
25
22
 
26
23
  *reader* also depends on the :mod:`sqlite3` standard library module
@@ -58,12 +55,18 @@ 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/
66
68
  .. _iso8601: http://pyiso8601.readthedocs.org/
69
+ .. _structlog: https://www.structlog.org/
67
70
  .. _typing-extensions: https://pypi.org/project/typing-extensions/
68
71
  .. _JSON1: https://www.sqlite.org/json1.html
69
72
  .. _FTS5: https://www.sqlite.org/fts5.html
@@ -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
Binary file
Binary file
@@ -40,10 +40,9 @@ dependencies = [
40
40
  "requests>=2.18",
41
41
  # for _http_utils
42
42
  "werkzeug>2",
43
- # for JSON Feed date parsing
44
- "iso8601>=1",
45
43
  # search
46
44
  "beautifulsoup4>=4.5",
45
+ "structlog",
47
46
  ]
48
47
  dynamic = ["version"]
49
48
 
@@ -60,7 +59,8 @@ Changelog = "https://reader.readthedocs.io/en/latest/changelog.html"
60
59
  all = ["reader[cli,app,unstable-plugins]"]
61
60
 
62
61
  cli = [
63
- "click>=7",
62
+ # != 8.4.0 because of https://github.com/pallets/click/issues/3458
63
+ "click>=7,!=8.4.0",
64
64
  ]
65
65
  app = [
66
66
  "flask>=0.10",
@@ -186,10 +186,15 @@ 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
192
193
 
194
+ [tool.black]
195
+ skip-string-normalization = true
196
+ extend-exclude = "src/reader/_vendor/.*"
197
+
193
198
  [tool.isort]
194
199
  profile = "black"
195
200
  py_version = 311
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.23'
40
+ __version__ = '3.25'
41
41
 
42
42
  # isort: off
43
43
 
@@ -67,6 +67,7 @@ from .types import (
67
67
  FeedToImport as FeedToImport,
68
68
  FeedImportResult as FeedImportResult,
69
69
  FeedExport as FeedExport,
70
+ Author as Author,
70
71
  )
71
72
 
72
73
  from .exceptions import (
@@ -106,6 +107,8 @@ USER_AGENT = f'python-reader/{__version__} (+{SOURCE_URL})'
106
107
  # Prevent any logging output by default. If no handler is set,
107
108
  # the messages bubble up to the root logger and get printed on stderr.
108
109
  # https://docs.python.org/3/howto/logging.html#library-config
110
+
109
111
  import logging # noqa: E402
112
+ from ._logging import enable_structlog as enable_structlog # noqa: E402
110
113
 
111
114
  logging.getLogger('reader').addHandler(logging.NullHandler())
@@ -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()
@@ -290,6 +325,10 @@ def import_feeds():
290
325
  parsed_feeds = [asdict(f) for f in opml.parse(file.stream)]
291
326
  except opml.OPMLError as e:
292
327
  error = str(e)
328
+ else:
329
+ if not parsed_feeds:
330
+ parsed_feeds = None
331
+ error = "file contains no feeds"
293
332
  else:
294
333
  feeds = (
295
334
  FeedToImport(**json.loads(f)) for f in request.form.getlist('feed')
@@ -312,6 +351,11 @@ def entry():
312
351
  return render_template('entry.html', entry=entry)
313
352
 
314
353
 
354
+ @blueprint.route('/help')
355
+ def help():
356
+ return render_template('help.html')
357
+
358
+
315
359
  def stream_template(template_name_or_list, **kwargs):
316
360
  # Ensure flashed messages get removed from the session,
317
361
  # otherwise they keep adding up and never disappear.
@@ -385,6 +429,11 @@ def _find_static(folder, filename):
385
429
  return matches[0]
386
430
 
387
431
 
432
+ @blueprint.app_template_global()
433
+ def is_new_user():
434
+ return not list(get_reader().get_feeds(limit=1))
435
+
436
+
388
437
  def create_app(reader_config):
389
438
  app = Flask(__name__)
390
439
  app.config['SECRET_KEY'] = 'secret'
@@ -18,7 +18,15 @@
18
18
  <ul class="controls">
19
19
 
20
20
  <li>
21
- {% if entry.author %} by {{ entry.author }}{% endif %}
21
+ {% if entry.authors %}
22
+ by
23
+ {% for author in entry.authors -%}
24
+ {% if author.href %}<a href="{{ author.href }}" title="{{ author.email or '' }}">{% endif %}
25
+ {{- author.name or author.email or 'unknown' -}}
26
+ {% if author.href %}</a>{% endif %}
27
+ {%- if not loop.last %}, {% endif %}
28
+ {%- endfor %}
29
+ {% endif %}
22
30
  in <a href="{{ url_for('.entries', feed=entry.feed.url) }}">{{ entry.feed_resolved_title or feed.url }}</a>
23
31
  <li>
24
32
  {%- set published = entry.published or entry.updated_not_none -%}
@@ -68,3 +68,9 @@
68
68
  max-width: 100%;
69
69
  height: auto;
70
70
  }
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);
76
+ }
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'add-feed' %}
1
2
  {% extends "layout.html" %}
2
3
 
3
4
  {% import "macros.html" as macros %}
@@ -12,7 +13,15 @@
12
13
 
13
14
  <h1>Add feed</h1>
14
15
 
15
- <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>
16
25
 
17
26
  <form action="{{ url_for(request.endpoint) }}" method="post" class="my-2 mb-4">
18
27
  {#- TODO: aria stuff #}
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'feeds' %}
1
2
  {% extends "layout.html" %}
2
3
 
3
4
  {% import "macros.html" as macros %}
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'feeds' %}
1
2
  {% extends "layout.html" %}
2
3
 
3
4
  {% import "macros.html" as macros %}