reader 3.23__tar.gz → 3.24__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 (258) hide show
  1. {reader-3.23 → reader-3.24}/CHANGES.rst +46 -0
  2. {reader-3.23/src/reader.egg-info → reader-3.24}/PKG-INFO +3 -3
  3. {reader-3.23 → reader-3.24}/docs/api.rst +8 -0
  4. {reader-3.23 → reader-3.24}/docs/app.rst +11 -0
  5. {reader-3.23 → reader-3.24}/docs/conf.py +3 -0
  6. {reader-3.23 → reader-3.24}/docs/guide.rst +31 -2
  7. {reader-3.23 → reader-3.24}/docs/install.rst +2 -1
  8. reader-3.24/docs/screenshots/empty.png +0 -0
  9. reader-3.24/docs/screenshots/help.png +0 -0
  10. reader-3.24/docs/screenshots/import-select.png +0 -0
  11. {reader-3.23 → reader-3.24}/pyproject.toml +7 -3
  12. {reader-3.23 → reader-3.24}/src/reader/__init__.py +4 -1
  13. {reader-3.23 → reader-3.24}/src/reader/_app/__init__.py +14 -0
  14. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/entry.html +9 -1
  15. {reader-3.23 → reader-3.24}/src/reader/_app/static/style.css +6 -0
  16. {reader-3.23 → reader-3.24}/src/reader/_app/templates/add_feed.html +1 -0
  17. {reader-3.23 → reader-3.24}/src/reader/_app/templates/change_feed_title.html +1 -0
  18. {reader-3.23 → reader-3.24}/src/reader/_app/templates/delete_feed.html +1 -0
  19. {reader-3.23 → reader-3.24}/src/reader/_app/templates/entries.html +16 -1
  20. {reader-3.23 → reader-3.24}/src/reader/_app/templates/entry.html +1 -0
  21. {reader-3.23 → reader-3.24}/src/reader/_app/templates/feeds.html +5 -1
  22. reader-3.24/src/reader/_app/templates/help.html +131 -0
  23. {reader-3.23 → reader-3.24}/src/reader/_app/templates/import_feeds.html +12 -2
  24. {reader-3.23 → reader-3.24}/src/reader/_app/templates/layout_base.html +11 -1
  25. {reader-3.23 → reader-3.24}/src/reader/_app/templates/macros.html +41 -3
  26. {reader-3.23 → reader-3.24}/src/reader/_cli.py +17 -6
  27. {reader-3.23 → reader-3.24}/src/reader/_config_utils.py +7 -3
  28. reader-3.24/src/reader/_logging.py +80 -0
  29. {reader-3.23 → reader-3.24}/src/reader/_parser/__init__.py +17 -9
  30. {reader-3.23 → reader-3.24}/src/reader/_parser/_lazy.py +50 -71
  31. {reader-3.23 → reader-3.24}/src/reader/_parser/feedparser.py +93 -12
  32. {reader-3.23 → reader-3.24}/src/reader/_parser/jsonfeed.py +29 -31
  33. {reader-3.23 → reader-3.24}/src/reader/_plugins/legacy/enclosure_tags.py +1 -1
  34. {reader-3.23 → reader-3.24}/src/reader/_storage/_entries.py +55 -19
  35. {reader-3.23 → reader-3.24}/src/reader/_storage/_feeds.py +65 -39
  36. {reader-3.23 → reader-3.24}/src/reader/_storage/_schema.py +45 -1
  37. {reader-3.23 → reader-3.24}/src/reader/_storage/_search.py +3 -13
  38. {reader-3.23 → reader-3.24}/src/reader/_storage/_sqlite_utils.py +2 -0
  39. {reader-3.23 → reader-3.24}/src/reader/_types.py +15 -99
  40. reader-3.23/src/reader/_update.py → reader-3.24/src/reader/_update/__init__.py +95 -196
  41. reader-3.24/src/reader/_update/base.py +205 -0
  42. reader-3.24/src/reader/_update/hooks.py +105 -0
  43. {reader-3.23 → reader-3.24}/src/reader/_utils.py +25 -61
  44. {reader-3.23 → reader-3.24}/src/reader/core.py +7 -36
  45. {reader-3.23 → reader-3.24}/src/reader/plugins/entry_dedupe.py +29 -40
  46. {reader-3.23 → reader-3.24}/src/reader/plugins/mark_as_read.py +5 -7
  47. {reader-3.23 → reader-3.24}/src/reader/plugins/readtime.py +8 -10
  48. {reader-3.23 → reader-3.24}/src/reader/plugins/ua_fallback.py +3 -10
  49. {reader-3.23 → reader-3.24}/src/reader/types.py +64 -9
  50. {reader-3.23 → reader-3.24/src/reader.egg-info}/PKG-INFO +3 -3
  51. {reader-3.23 → reader-3.24}/src/reader.egg-info/SOURCES.txt +9 -1
  52. {reader-3.23 → reader-3.24}/src/reader.egg-info/requires.txt +2 -2
  53. {reader-3.23 → reader-3.24}/tests/conftest.py +3 -39
  54. {reader-3.23 → reader-3.24}/tests/data/empty.atom.py +1 -0
  55. {reader-3.23 → reader-3.24}/tests/data/full.atom.py +4 -3
  56. {reader-3.23 → reader-3.24}/tests/data/full.json.py +7 -3
  57. {reader-3.23 → reader-3.24}/tests/data/full.rss.py +3 -2
  58. {reader-3.23 → reader-3.24}/tests/data/invalid.json.py +3 -2
  59. reader-3.24/tests/fixtures.py +48 -0
  60. {reader-3.23 → reader-3.24}/tests/test__types.py +14 -5
  61. {reader-3.23 → reader-3.24}/tests/test__utils.py +0 -19
  62. {reader-3.23 → reader-3.24}/tests/test_cli.py +18 -4
  63. {reader-3.23 → reader-3.24}/tests/test_config_utils.py +8 -0
  64. {reader-3.23 → reader-3.24}/tests/test_lazy_imports.py +0 -2
  65. reader-3.24/tests/test_logging.py +41 -0
  66. {reader-3.23 → reader-3.24}/tests/test_parser.py +201 -13
  67. {reader-3.23 → reader-3.24}/tests/test_plugins_ua_fallback.py +1 -1
  68. {reader-3.23 → reader-3.24}/tests/test_reader.py +16 -13
  69. {reader-3.23 → reader-3.24}/tests/test_reader_search.py +7 -2
  70. {reader-3.23 → reader-3.24}/tests/test_reader_update.py +11 -9
  71. {reader-3.23 → reader-3.24}/tests/test_storage.py +28 -0
  72. {reader-3.23 → reader-3.24}/tests/test_types.py +33 -0
  73. reader-3.23/docs/screenshots/import-select.png +0 -0
  74. {reader-3.23 → reader-3.24}/LICENSE +0 -0
  75. {reader-3.23 → reader-3.24}/MANIFEST.in +0 -0
  76. {reader-3.23 → reader-3.24}/README.rst +0 -0
  77. {reader-3.23 → reader-3.24}/docs/changelog.rst +0 -0
  78. {reader-3.23 → reader-3.24}/docs/cli.rst +0 -0
  79. {reader-3.23 → reader-3.24}/docs/config.rst +0 -0
  80. {reader-3.23 → reader-3.24}/docs/contributing.rst +0 -0
  81. {reader-3.23 → reader-3.24}/docs/dev.rst +0 -0
  82. {reader-3.23 → reader-3.24}/docs/index.rst +0 -0
  83. {reader-3.23 → reader-3.24}/docs/internal.rst +0 -0
  84. {reader-3.23 → reader-3.24}/docs/plugins.rst +0 -0
  85. {reader-3.23 → reader-3.24}/docs/screenshots/entries-filters-light.png +0 -0
  86. {reader-3.23 → reader-3.24}/docs/screenshots/entries.png +0 -0
  87. {reader-3.23 → reader-3.24}/docs/screenshots/entry-image-light.png +0 -0
  88. {reader-3.23 → reader-3.24}/docs/screenshots/entry.png +0 -0
  89. {reader-3.23 → reader-3.24}/docs/screenshots/feed.png +0 -0
  90. {reader-3.23 → reader-3.24}/docs/screenshots/feeds.png +0 -0
  91. {reader-3.23 → reader-3.24}/docs/screenshots/import-result.png +0 -0
  92. {reader-3.23 → reader-3.24}/docs/tutorial.rst +0 -0
  93. {reader-3.23 → reader-3.24}/docs/why.rst +0 -0
  94. {reader-3.23 → reader-3.24}/examples/config.toml +0 -0
  95. {reader-3.23 → reader-3.24}/examples/custom_headers.py +0 -0
  96. {reader-3.23 → reader-3.24}/examples/feed_slugs.py +0 -0
  97. {reader-3.23 → reader-3.24}/examples/parser_only.py +0 -0
  98. {reader-3.23 → reader-3.24}/examples/podcast.py +0 -0
  99. {reader-3.23 → reader-3.24}/examples/terminal.py +0 -0
  100. {reader-3.23 → reader-3.24}/run.sh +0 -0
  101. {reader-3.23 → reader-3.24}/scripts/backup.sh +0 -0
  102. {reader-3.23 → reader-3.24}/scripts/bench.py +0 -0
  103. {reader-3.23 → reader-3.24}/scripts/debug_storage_stats.py +0 -0
  104. {reader-3.23 → reader-3.24}/scripts/entry_dedupe_backtest.py +0 -0
  105. {reader-3.23 → reader-3.24}/scripts/jscontrols.html +0 -0
  106. {reader-3.23 → reader-3.24}/scripts/jscontrols.py +0 -0
  107. {reader-3.23 → reader-3.24}/scripts/lines.sh +0 -0
  108. {reader-3.23 → reader-3.24}/scripts/release.py +0 -0
  109. {reader-3.23 → reader-3.24}/scripts/vendor_app_stuff.py +0 -0
  110. {reader-3.23 → reader-3.24}/setup.cfg +0 -0
  111. {reader-3.23 → reader-3.24}/src/reader/__main__.py +0 -0
  112. {reader-3.23 → reader-3.24}/src/reader/_app/cli.py +0 -0
  113. {reader-3.23 → reader-3.24}/src/reader/_app/ext.py +0 -0
  114. {reader-3.23 → reader-3.24}/src/reader/_app/forms.py +0 -0
  115. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/__init__.py +0 -0
  116. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/api_thing.py +0 -0
  117. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/static/controls.js +0 -0
  118. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/static/style.css +0 -0
  119. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  120. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/entries.html +0 -0
  121. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/feeds.html +0 -0
  122. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/layout.html +0 -0
  123. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/macros.html +0 -0
  124. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/metadata.html +0 -0
  125. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/templates/tags.html +0 -0
  126. {reader-3.23 → reader-3.24}/src/reader/_app/legacy/wsgi.py +0 -0
  127. {reader-3.23 → reader-3.24}/src/reader/_app/static/theme.js +0 -0
  128. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/README +0 -0
  129. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  130. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  131. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  132. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  133. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  134. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  135. {reader-3.23 → reader-3.24}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  136. {reader-3.23 → reader-3.24}/src/reader/_app/templates/layout.html +0 -0
  137. {reader-3.23 → reader-3.24}/src/reader/_app/wsgi.py +0 -0
  138. {reader-3.23 → reader-3.24}/src/reader/_hash_utils.py +0 -0
  139. {reader-3.23 → reader-3.24}/src/reader/_parser/_http_utils.py +0 -0
  140. {reader-3.23 → reader-3.24}/src/reader/_parser/_url_utils.py +0 -0
  141. {reader-3.23 → reader-3.24}/src/reader/_parser/file.py +0 -0
  142. {reader-3.23 → reader-3.24}/src/reader/_parser/http.py +0 -0
  143. {reader-3.23 → reader-3.24}/src/reader/_parser/requests/__init__.py +0 -0
  144. {reader-3.23 → reader-3.24}/src/reader/_parser/requests/_lazy.py +0 -0
  145. {reader-3.23 → reader-3.24}/src/reader/_plugins/__init__.py +0 -0
  146. {reader-3.23 → reader-3.24}/src/reader/_plugins/cli_status.py +0 -0
  147. {reader-3.23 → reader-3.24}/src/reader/_plugins/legacy/__init__.py +0 -0
  148. {reader-3.23 → reader-3.24}/src/reader/_plugins/legacy/preview_feed_list.py +0 -0
  149. {reader-3.23 → reader-3.24}/src/reader/_plugins/legacy/share.py +0 -0
  150. {reader-3.23 → reader-3.24}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  151. {reader-3.23 → reader-3.24}/src/reader/_plugins/sqlite_releases.py +0 -0
  152. {reader-3.23 → reader-3.24}/src/reader/_plugins/timer.py +0 -0
  153. {reader-3.23 → reader-3.24}/src/reader/_storage/__init__.py +0 -0
  154. {reader-3.23 → reader-3.24}/src/reader/_storage/_base.py +0 -0
  155. {reader-3.23 → reader-3.24}/src/reader/_storage/_changes.py +0 -0
  156. {reader-3.23 → reader-3.24}/src/reader/_storage/_html_utils.py +0 -0
  157. {reader-3.23 → reader-3.24}/src/reader/_storage/_sql_utils.py +0 -0
  158. {reader-3.23 → reader-3.24}/src/reader/_storage/_tags.py +0 -0
  159. {reader-3.23 → reader-3.24}/src/reader/_vendor/__init__.py +0 -0
  160. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/__init__.py +0 -0
  161. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/api.py +0 -0
  162. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  163. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  164. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  165. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  166. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  167. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  168. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  169. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  170. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  171. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/encodings.py +0 -0
  172. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  173. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/html.py +0 -0
  174. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/http.py +0 -0
  175. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/mixin.py +0 -0
  176. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  177. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  178. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  179. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  180. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  181. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  182. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  183. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  184. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  185. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  186. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  187. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  188. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  189. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/py.typed +0 -0
  190. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  191. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/sgml.py +0 -0
  192. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/urls.py +0 -0
  193. {reader-3.23 → reader-3.24}/src/reader/_vendor/feedparser/util.py +0 -0
  194. {reader-3.23 → reader-3.24}/src/reader/exceptions.py +0 -0
  195. {reader-3.23 → reader-3.24}/src/reader/opml.py +0 -0
  196. {reader-3.23 → reader-3.24}/src/reader/plugins/__init__.py +0 -0
  197. {reader-3.23 → reader-3.24}/src/reader/plugins/_loader.py +0 -0
  198. {reader-3.23 → reader-3.24}/src/reader/plugins/enclosure_dedupe.py +0 -0
  199. {reader-3.23 → reader-3.24}/src/reader/py.typed +0 -0
  200. {reader-3.23 → reader-3.24}/src/reader/utils.py +0 -0
  201. {reader-3.23 → reader-3.24}/src/reader.egg-info/dependency_links.txt +0 -0
  202. {reader-3.23 → reader-3.24}/src/reader.egg-info/top_level.txt +0 -0
  203. {reader-3.23 → reader-3.24}/tests/_plugins/__init__.py +0 -0
  204. {reader-3.23 → reader-3.24}/tests/_plugins/builtin.py +0 -0
  205. {reader-3.23 → reader-3.24}/tests/_plugins/error_during_import.py +0 -0
  206. {reader-3.23 → reader-3.24}/tests/_plugins/error_import_error.py +0 -0
  207. {reader-3.23 → reader-3.24}/tests/data/10.json +0 -0
  208. {reader-3.23 → reader-3.24}/tests/data/10.json.py +0 -0
  209. {reader-3.23 → reader-3.24}/tests/data/custom +0 -0
  210. {reader-3.23 → reader-3.24}/tests/data/empty.atom +0 -0
  211. {reader-3.23 → reader-3.24}/tests/data/empty.json +0 -0
  212. {reader-3.23 → reader-3.24}/tests/data/empty.json.py +0 -0
  213. {reader-3.23 → reader-3.24}/tests/data/empty.rss +0 -0
  214. {reader-3.23 → reader-3.24}/tests/data/empty.rss.py +0 -0
  215. {reader-3.23 → reader-3.24}/tests/data/full.atom +0 -0
  216. {reader-3.23 → reader-3.24}/tests/data/full.json +0 -0
  217. {reader-3.23 → reader-3.24}/tests/data/full.rss +0 -0
  218. {reader-3.23 → reader-3.24}/tests/data/invalid.json +0 -0
  219. {reader-3.23 → reader-3.24}/tests/data/relative.atom +0 -0
  220. {reader-3.23 → reader-3.24}/tests/data/relative.atom.py +0 -0
  221. {reader-3.23 → reader-3.24}/tests/data/relative.rss +0 -0
  222. {reader-3.23 → reader-3.24}/tests/data/relative.rss.py +0 -0
  223. {reader-3.23 → reader-3.24}/tests/data/sqlite_releases.html +0 -0
  224. {reader-3.23 → reader-3.24}/tests/data/unknown.json +0 -0
  225. {reader-3.23 → reader-3.24}/tests/data/unknown.json.py +0 -0
  226. {reader-3.23 → reader-3.24}/tests/fakeparser.py +0 -0
  227. {reader-3.23 → reader-3.24}/tests/reader_methods.py +0 -0
  228. {reader-3.23 → reader-3.24}/tests/test_app_legacy.py +0 -0
  229. {reader-3.23 → reader-3.24}/tests/test_app_wsgi.py +0 -0
  230. {reader-3.23 → reader-3.24}/tests/test_bench.py +0 -0
  231. {reader-3.23 → reader-3.24}/tests/test_changes.py +0 -0
  232. {reader-3.23 → reader-3.24}/tests/test_exceptions.py +0 -0
  233. {reader-3.23 → reader-3.24}/tests/test_hash_utils.py +0 -0
  234. {reader-3.23 → reader-3.24}/tests/test_html_utils.py +0 -0
  235. {reader-3.23 → reader-3.24}/tests/test_opml.py +0 -0
  236. {reader-3.23 → reader-3.24}/tests/test_plugins_cli_status.py +0 -0
  237. {reader-3.23 → reader-3.24}/tests/test_plugins_enclosure_dedupe.py +0 -0
  238. {reader-3.23 → reader-3.24}/tests/test_plugins_entry_dedupe.py +0 -0
  239. {reader-3.23 → reader-3.24}/tests/test_plugins_loader.py +0 -0
  240. {reader-3.23 → reader-3.24}/tests/test_plugins_mark_as_read.py +0 -0
  241. {reader-3.23 → reader-3.24}/tests/test_plugins_preview_feed_list.py +0 -0
  242. {reader-3.23 → reader-3.24}/tests/test_plugins_readtime.py +0 -0
  243. {reader-3.23 → reader-3.24}/tests/test_plugins_sqlite_releases.py +0 -0
  244. {reader-3.23 → reader-3.24}/tests/test_reader_counts.py +0 -0
  245. {reader-3.23 → reader-3.24}/tests/test_reader_deprecations.py +0 -0
  246. {reader-3.23 → reader-3.24}/tests/test_reader_filter.py +0 -0
  247. {reader-3.23 → reader-3.24}/tests/test_reader_hooks.py +0 -0
  248. {reader-3.23 → reader-3.24}/tests/test_reader_integration.py +0 -0
  249. {reader-3.23 → reader-3.24}/tests/test_reader_lifecycle.py +0 -0
  250. {reader-3.23 → reader-3.24}/tests/test_reader_private.py +0 -0
  251. {reader-3.23 → reader-3.24}/tests/test_reader_sort.py +0 -0
  252. {reader-3.23 → reader-3.24}/tests/test_reader_utils.py +0 -0
  253. {reader-3.23 → reader-3.24}/tests/test_search.py +0 -0
  254. {reader-3.23 → reader-3.24}/tests/test_sql_utils.py +0 -0
  255. {reader-3.23 → reader-3.24}/tests/test_sqlite_utils.py +0 -0
  256. {reader-3.23 → reader-3.24}/tests/test_tags.py +0 -0
  257. {reader-3.23 → reader-3.24}/tests/utils.py +0 -0
  258. {reader-3.23 → reader-3.24}/tox.ini +0 -0
@@ -6,6 +6,52 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.24
10
+ ------------
11
+
12
+ Released 2026-05-20
13
+
14
+ * Support multiple authors with details (link, email).
15
+ Thanks to `Anshul Mittal`_ for the PR.
16
+ (:issue:`391`)
17
+
18
+ * Add :attr:`Feed.authors`, :attr:`Entry.authors`,
19
+ and :attr:`EntrySource.authors` attributes.
20
+ * Add matching :attr:`~Feed.authors_str` property.
21
+ * Deprecate :attr:`~Feed.author` attribute.
22
+
23
+ * Fix bug introduced in `version 3.15 <Version 3.15_>`__
24
+ causing feeds to not be retrieved in parallel
25
+ even if :meth:`~Reader.update_feeds` was called with ``workers`` > 1.
26
+
27
+ * Use :ref:`structured logging <logging>` internally,
28
+ with the possibility to opt into native `structlog`_ logging.
29
+ (:issue:`147`)
30
+
31
+ * Use :meth:`datetime.datetime.fromisoformat` instead of external dependency ``iso8601``.
32
+ Thanks to `Sebastian Bradley`_ for the PR.
33
+ (:issue:`398`)
34
+
35
+ * Refactor updater internals to allow reusing logic in alternate implementations.
36
+
37
+ * :doc:`app` improvements.
38
+
39
+ * Add empty state and contextual help; :ref:`screenshots <screenshots-help>`.
40
+ * Import feeds improvements (select all/none, accept only XML / OPML files).
41
+
42
+ * Add ``--json`` option to the ``list`` CLI commands.
43
+ Thanks to `Puneet Dixit`_ for the PR.
44
+ (:issue:`394`)
45
+
46
+
47
+
48
+ .. _Puneet Dixit: https://github.com/puneetdixit200
49
+ .. _Anshul Mittal: https://github.com/anderson688
50
+ .. _Sebastian Bradley: https://github.com/bazbrad765
51
+
52
+ .. _structlog: https://www.structlog.org/
53
+
54
+
9
55
  Version 3.23
10
56
  ------------
11
57
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.23
3
+ Version: 3.24
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"
@@ -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
 
@@ -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,6 +19,9 @@ 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
26
  sys.modules[name] = unittest.mock.Mock()
24
27
 
@@ -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
@@ -18,9 +18,9 @@ These packages will be installed automatically when installing *reader*:
18
18
  it replaces feedparser's default use of :mod:`urllib`
19
19
  to make it easier to write plugins.
20
20
  * `werkzeug`_ provides HTTP utilities.
21
- * `iso8601`_ parses dates in ISO 8601 / RFC 3339; used for JSON Feed parsing.
22
21
  * `beautifulsoup4`_ is used to strip HTML tags before adding entries
23
22
  to the search index.
23
+ * `structlog`_ is used for structured logging.
24
24
  * `typing-extensions`_ is used for :mod:`typing` backports.
25
25
 
26
26
  *reader* also depends on the :mod:`sqlite3` standard library module
@@ -64,6 +64,7 @@ As of version |version|, *reader* has the following extras:
64
64
  .. _requests: https://requests.readthedocs.io/
65
65
  .. _werkzeug: https://werkzeug.palletsprojects.com/
66
66
  .. _iso8601: http://pyiso8601.readthedocs.org/
67
+ .. _structlog: https://www.structlog.org/
67
68
  .. _typing-extensions: https://pypi.org/project/typing-extensions/
68
69
  .. _JSON1: https://www.sqlite.org/json1.html
69
70
  .. _FTS5: https://www.sqlite.org/fts5.html
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",
@@ -190,6 +190,10 @@ module = [
190
190
  ]
191
191
  ignore_errors = true
192
192
 
193
+ [tool.black]
194
+ skip-string-normalization = true
195
+ extend-exclude = "src/reader/_vendor/.*"
196
+
193
197
  [tool.isort]
194
198
  profile = "black"
195
199
  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.24'
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())
@@ -290,6 +290,10 @@ def import_feeds():
290
290
  parsed_feeds = [asdict(f) for f in opml.parse(file.stream)]
291
291
  except opml.OPMLError as e:
292
292
  error = str(e)
293
+ else:
294
+ if not parsed_feeds:
295
+ parsed_feeds = None
296
+ error = "file contains no feeds"
293
297
  else:
294
298
  feeds = (
295
299
  FeedToImport(**json.loads(f)) for f in request.form.getlist('feed')
@@ -312,6 +316,11 @@ def entry():
312
316
  return render_template('entry.html', entry=entry)
313
317
 
314
318
 
319
+ @blueprint.route('/help')
320
+ def help():
321
+ return render_template('help.html')
322
+
323
+
315
324
  def stream_template(template_name_or_list, **kwargs):
316
325
  # Ensure flashed messages get removed from the session,
317
326
  # otherwise they keep adding up and never disappear.
@@ -385,6 +394,11 @@ def _find_static(folder, filename):
385
394
  return matches[0]
386
395
 
387
396
 
397
+ @blueprint.app_template_global()
398
+ def is_new_user():
399
+ return not list(get_reader().get_feeds(limit=1))
400
+
401
+
388
402
  def create_app(reader_config):
389
403
  app = Flask(__name__)
390
404
  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 %}
@@ -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 %}
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'entries' if request.args else None %}
1
2
  {% extends "layout.html" %}
2
3
 
3
4
  {% import "macros.html" as macros %}
@@ -142,6 +143,17 @@
142
143
  <i class="bi bi-{{ macros.bs_file_icon(enclosure.type) }}"></i>
143
144
  </a>
144
145
  {%- endfor %}
146
+ {%- if loop is defined and loop.first %}
147
+ <a
148
+ href="{{ url_for('.help', _anchor='entry-flags') }}"
149
+ target="_blank"
150
+ title="help: entry flags"
151
+ class="btn text-secondary"
152
+ role="button"
153
+ >
154
+ <i class="bi bi-question-lg"></i>
155
+ </a>
156
+ {%- endif %}
145
157
  </div>
146
158
 
147
159
  {% endblock %}
@@ -150,7 +162,10 @@
150
162
  </div>
151
163
 
152
164
  {% else %}
153
- <p>no entries found</p>
165
+ <p>No entries found.</p>
166
+ {%- if is_new_user() %}
167
+ {{ macros.new_user_help_alert() }}
168
+ {% endif %}
154
169
  {% endfor %}
155
170
 
156
171
 
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'entries' %}
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 %}
@@ -76,7 +77,10 @@
76
77
  </div>
77
78
 
78
79
  {% else %}
79
- <p>no entries found</p>
80
+ <p>No feeds found.</p>
81
+ {%- if is_new_user() %}
82
+ {{ macros.new_user_help_alert() }}
83
+ {%- endif %}
80
84
  {% endfor %}
81
85
 
82
86
 
@@ -0,0 +1,131 @@
1
+ {% extends "layout.html" %}
2
+
3
+ {% import "macros.html" as macros %}
4
+
5
+
6
+ {% block page_title %}
7
+ Help - {{ super() }}
8
+ {% endblock %}
9
+
10
+
11
+ {% block body %}
12
+
13
+ <h1>Help</h1>
14
+
15
+
16
+ <p>
17
+ Welcome to <strong>reader</strong>, a feed reader web app!
18
+
19
+ <p>
20
+ <strong>reader</strong> allows you to
21
+ subscribe to web feeds,
22
+ read articles,
23
+ and keep track of the ones you've read or liked.
24
+
25
+
26
+ <h2 id="feeds">Feeds</h2>
27
+
28
+ <p>
29
+ A <a href="https://en.wikipedia.org/wiki/Web_feed">web feed</a>
30
+ is an internet document used to get updates
31
+ from websites such as blogs, web comics, or podcasts.
32
+
33
+ <p>
34
+ When you subscribe to a feed,
35
+ <em>reader</em> checks it regularly to get the latest articles
36
+ (unless the feed is disabled).
37
+
38
+
39
+ <h3 id="add-feed">Adding feeds</h3>
40
+
41
+ <p>
42
+ To add a feed, paste its link in the
43
+ <a href="{{ url_for('.add_feed') }}">Add feed</a> page.
44
+
45
+ <p>
46
+ You can usually find if a website has a feed
47
+ by looking in its header or footer for this icon
48
+ <i class="bi bi-rss" aria-label="RSS logo"></i>,
49
+ or for links called <em>feed</em>, <em>RSS</em>, or <em>Atom</em>.
50
+
51
+
52
+ <h3 id="import-export">Importing / exporting feeds</h3>
53
+
54
+ <p>
55
+ You can <a href="{{ url_for('.import_feeds') }}">import feeds</a>
56
+ from a subscription list exported by another feed reader.
57
+
58
+ <p>
59
+ To export your own feeds as a subscription list, click the
60
+ <i class="bi bi-filetype-xml" aria-label="export as OPML"></i> icon
61
+ on the <a href="{{ url_for('.feeds') }}">Feeds</a> page.
62
+
63
+
64
+ <h2 id="entries">Entries</h2>
65
+
66
+ <p>
67
+ An entry is an item from a feed –
68
+ an article, a podcast episode, etc.
69
+
70
+ <p>
71
+ To read an entry, click its title on the
72
+ <a href="{{ url_for('.import_feeds') }}">Entries</a> page.
73
+
74
+ Some feeds only include summaries, not the full content; click the
75
+ <i class="bi bi-box-arrow-up-right" aria-label="extenal link"></i> icon
76
+ to go to the original article.
77
+
78
+
79
+ <h3 id="entry-flags">Entry flags</h3>
80
+
81
+ <p>
82
+ Each entry has a <em>read</em> and an <em>important</em> flag.
83
+ Among others, they are used to filter entries
84
+ on the <a href="{{ url_for('.entries') }}">Entries</a> page.
85
+
86
+ <p>
87
+ You can set the flags by using the toggles below the entry title.
88
+
89
+ <dl>
90
+
91
+ <dt>
92
+ <i class="bi bi-check-lg me-1"></i> mark as read / mark as unread
93
+ <dd>
94
+ Mark the entry as read.
95
+ Hides the entry from the default entries view.
96
+
97
+ <dt>
98
+ <i class="bi bi-star me-1"></i> mark as important / clear important
99
+ <dd>
100
+ Mark the entry as important.
101
+ Important entries are never deleted.
102
+ <small>
103
+ (Currently, entries are never deleted,
104
+ but an upcoming feature will clean up old entries.)
105
+ </small>
106
+
107
+ <dt>
108
+ <i class="bi bi-x-lg me-1"></i> don't care / clear don't care
109
+ <dd>
110
+ Mark the entry as explicitly <em>not</em> important.
111
+ Hides the entry from the default entries view without marking it as read.
112
+ <small>
113
+ (An upcoming feature will allow identifying "noisy" feeds based on this.)
114
+ </small>
115
+
116
+ </dl>
117
+
118
+
119
+ <h3 id="enclosures">Enclosures</h3>
120
+
121
+ <p>
122
+ Some entries have external resources you can download,
123
+ for example an audio file of a podcast episode.
124
+
125
+ <p>
126
+ Click the
127
+ <i class="bi bi-file-earmark-music" aria-label="download ..."></i> icon
128
+ to download the first enclosure of an entry, if any.
129
+
130
+
131
+ {% endblock %}
@@ -1,3 +1,4 @@
1
+ {% set help_topic = 'import-export' %}
1
2
  {% extends "layout.html" %}
2
3
 
3
4
  {% import "macros.html" as macros %}
@@ -24,15 +25,16 @@
24
25
 
25
26
  {% if parsed_feeds is none and imported_feeds is none %}
26
27
 
27
- <p>Upload OPML subscription list:</p>
28
+ <p>Upload an OPML subscription list:</p>
28
29
 
29
30
  <div class="mb-3">
30
31
 
31
- <!-- TODO accept -->
32
32
  <input
33
33
  class="form-control{% if error %} is-invalid {% endif %}"
34
34
  type="file"
35
35
  name="file"
36
+ accept="application/xml, text/xml, text/x-opml, .xml, .opml"
37
+ required
36
38
  >
37
39
  {%- if error %}
38
40
  <div class="invalid-feedback">
@@ -52,6 +54,10 @@
52
54
 
53
55
  <p>Select feeds to import:</p>
54
56
 
57
+ <div class="mb-3">
58
+ {{ macros.select_all_none() }}
59
+ </div>
60
+
55
61
  {% for feed in parsed_feeds %}
56
62
  <div class="form-check mb-3">
57
63
  <input
@@ -79,6 +85,10 @@
79
85
  </div>
80
86
  {% endfor %}
81
87
 
88
+ <div class="mb-3">
89
+ {{ macros.select_all_none() }}
90
+ </div>
91
+
82
92
  <input type="submit" class="btn btn-primary" value="import selected">
83
93
 
84
94
  <a href="javascript:history.back()" class="btn btn-outline-secondary" role="button" >
@@ -62,12 +62,22 @@
62
62
  <ul class="navbar-nav mb-0">
63
63
  {% block right_hidden_navbar %}
64
64
 
65
+ <li class="nav-item">
66
+ <a
67
+ href="{{ url_for('reader.help', _anchor=help_topic | default(none)) }}"
68
+ class="btn btn-link nav-link d-flex align-items-center"
69
+ >
70
+ <i class="bi bi-question-lg"></i>
71
+ <span class="d-md-none ms-2" id="theme-text">help</span>
72
+ </a>
73
+ </li>
74
+
65
75
  <li class="nav-item dropdown">
66
76
  <button
67
77
  class="btn btn-link nav-link dropdown-toggle d-flex align-items-center"
68
78
  type="button" id="theme"
69
79
  data-bs-toggle="dropdown" data-bs-display="static"
70
- aria-expanded="false" aria-label="toggle theme (auto)">
80
+ aria-expanded="false" aria-label="toggle theme (auto)">
71
81
  <i class="bi bi-circle-half theme-icon-active"></i>
72
82
  <span class="d-md-none ms-2" id="theme-text">toggle theme</span>
73
83
  </button>