reader 3.22__tar.gz → 3.23__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 (250) hide show
  1. {reader-3.22 → reader-3.23}/CHANGES.rst +16 -0
  2. {reader-3.22/src/reader.egg-info → reader-3.23}/PKG-INFO +2 -1
  3. {reader-3.22 → reader-3.23}/README.rst +1 -0
  4. {reader-3.22 → reader-3.23}/docs/api.rst +21 -0
  5. {reader-3.22 → reader-3.23}/docs/app.rst +12 -0
  6. {reader-3.22 → reader-3.23}/docs/conf.py +0 -2
  7. {reader-3.22 → reader-3.23}/docs/dev.rst +0 -1
  8. {reader-3.22 → reader-3.23}/docs/guide.rst +52 -23
  9. reader-3.23/docs/screenshots/feeds.png +0 -0
  10. reader-3.23/docs/screenshots/import-result.png +0 -0
  11. reader-3.23/docs/screenshots/import-select.png +0 -0
  12. {reader-3.22 → reader-3.23}/examples/podcast.py +0 -1
  13. {reader-3.22 → reader-3.23}/pyproject.toml +0 -1
  14. {reader-3.22 → reader-3.23}/scripts/bench.py +0 -1
  15. {reader-3.22 → reader-3.23}/scripts/debug_storage_stats.py +0 -1
  16. {reader-3.22 → reader-3.23}/scripts/entry_dedupe_backtest.py +0 -1
  17. {reader-3.22 → reader-3.23}/scripts/jscontrols.py +0 -2
  18. {reader-3.22 → reader-3.23}/scripts/vendor_app_stuff.py +0 -1
  19. {reader-3.22 → reader-3.23}/src/reader/__init__.py +7 -3
  20. {reader-3.22 → reader-3.23}/src/reader/__main__.py +0 -1
  21. {reader-3.22 → reader-3.23}/src/reader/_app/__init__.py +36 -1
  22. {reader-3.22 → reader-3.23}/src/reader/_app/ext.py +0 -1
  23. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/__init__.py +0 -1
  24. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/wsgi.py +0 -1
  25. {reader-3.22 → reader-3.23}/src/reader/_app/templates/add_feed.html +9 -1
  26. {reader-3.22 → reader-3.23}/src/reader/_app/templates/feeds.html +8 -1
  27. reader-3.23/src/reader/_app/templates/import_feeds.html +121 -0
  28. {reader-3.22 → reader-3.23}/src/reader/_app/templates/layout_base.html +3 -0
  29. {reader-3.22 → reader-3.23}/src/reader/_app/templates/macros.html +22 -7
  30. {reader-3.22 → reader-3.23}/src/reader/_app/wsgi.py +0 -1
  31. {reader-3.22 → reader-3.23}/src/reader/_cli.py +0 -1
  32. {reader-3.22 → reader-3.23}/src/reader/_hash_utils.py +0 -1
  33. {reader-3.22 → reader-3.23}/src/reader/_parser/__init__.py +0 -1
  34. {reader-3.22 → reader-3.23}/src/reader/_parser/_http_utils.py +0 -1
  35. {reader-3.22 → reader-3.23}/src/reader/_parser/_lazy.py +0 -1
  36. {reader-3.22 → reader-3.23}/src/reader/_parser/feedparser.py +0 -1
  37. {reader-3.22 → reader-3.23}/src/reader/_parser/jsonfeed.py +0 -1
  38. {reader-3.22 → reader-3.23}/src/reader/_parser/requests/__init__.py +0 -1
  39. {reader-3.22 → reader-3.23}/src/reader/_parser/requests/_lazy.py +0 -1
  40. {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/enclosure_tags.py +0 -1
  41. {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/preview_feed_list.py +0 -1
  42. {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/share.py +0 -1
  43. {reader-3.22 → reader-3.23}/src/reader/_plugins/sqlite_releases.py +0 -1
  44. {reader-3.22 → reader-3.23}/src/reader/_storage/__init__.py +0 -1
  45. {reader-3.22 → reader-3.23}/src/reader/_storage/_base.py +0 -1
  46. {reader-3.22 → reader-3.23}/src/reader/_storage/_changes.py +2 -5
  47. {reader-3.22 → reader-3.23}/src/reader/_storage/_entries.py +7 -12
  48. {reader-3.22 → reader-3.23}/src/reader/_storage/_feeds.py +1 -2
  49. {reader-3.22 → reader-3.23}/src/reader/_storage/_html_utils.py +0 -1
  50. {reader-3.22 → reader-3.23}/src/reader/_storage/_schema.py +4 -9
  51. {reader-3.22 → reader-3.23}/src/reader/_storage/_search.py +13 -22
  52. {reader-3.22 → reader-3.23}/src/reader/_storage/_sql_utils.py +0 -1
  53. {reader-3.22 → reader-3.23}/src/reader/_storage/_sqlite_utils.py +0 -1
  54. {reader-3.22 → reader-3.23}/src/reader/_storage/_tags.py +0 -1
  55. {reader-3.22 → reader-3.23}/src/reader/_types.py +0 -1
  56. {reader-3.22 → reader-3.23}/src/reader/_update.py +0 -1
  57. {reader-3.22 → reader-3.23}/src/reader/_utils.py +0 -1
  58. {reader-3.22 → reader-3.23}/src/reader/core.py +79 -1
  59. {reader-3.22 → reader-3.23}/src/reader/exceptions.py +8 -0
  60. reader-3.23/src/reader/opml.py +178 -0
  61. {reader-3.22 → reader-3.23}/src/reader/plugins/__init__.py +0 -1
  62. {reader-3.22 → reader-3.23}/src/reader/plugins/_loader.py +0 -1
  63. {reader-3.22 → reader-3.23}/src/reader/plugins/entry_dedupe.py +2 -5
  64. {reader-3.22 → reader-3.23}/src/reader/plugins/mark_as_read.py +0 -1
  65. {reader-3.22 → reader-3.23}/src/reader/plugins/readtime.py +0 -1
  66. {reader-3.22 → reader-3.23}/src/reader/plugins/ua_fallback.py +0 -1
  67. {reader-3.22 → reader-3.23}/src/reader/types.py +70 -1
  68. {reader-3.22 → reader-3.23}/src/reader/utils.py +16 -2
  69. {reader-3.22 → reader-3.23/src/reader.egg-info}/PKG-INFO +2 -1
  70. {reader-3.22 → reader-3.23}/src/reader.egg-info/SOURCES.txt +5 -0
  71. {reader-3.22 → reader-3.23}/tests/data/10.json.py +0 -1
  72. {reader-3.22 → reader-3.23}/tests/data/empty.atom.py +0 -1
  73. {reader-3.22 → reader-3.23}/tests/data/empty.json.py +0 -1
  74. {reader-3.22 → reader-3.23}/tests/data/empty.rss.py +0 -1
  75. {reader-3.22 → reader-3.23}/tests/data/full.atom.py +0 -1
  76. {reader-3.22 → reader-3.23}/tests/data/full.json.py +0 -1
  77. {reader-3.22 → reader-3.23}/tests/data/full.rss.py +0 -1
  78. {reader-3.22 → reader-3.23}/tests/data/invalid.json.py +0 -1
  79. {reader-3.22 → reader-3.23}/tests/data/relative.atom.py +0 -1
  80. {reader-3.22 → reader-3.23}/tests/data/relative.rss.py +0 -1
  81. {reader-3.22 → reader-3.23}/tests/data/unknown.json.py +0 -1
  82. {reader-3.22 → reader-3.23}/tests/test__types.py +0 -1
  83. {reader-3.22 → reader-3.23}/tests/test__utils.py +0 -1
  84. {reader-3.22 → reader-3.23}/tests/test_app_legacy.py +0 -1
  85. {reader-3.22 → reader-3.23}/tests/test_bench.py +0 -2
  86. {reader-3.22 → reader-3.23}/tests/test_changes.py +0 -1
  87. {reader-3.22 → reader-3.23}/tests/test_config_utils.py +2 -4
  88. {reader-3.22 → reader-3.23}/tests/test_html_utils.py +0 -1
  89. {reader-3.22 → reader-3.23}/tests/test_lazy_imports.py +2 -5
  90. reader-3.23/tests/test_opml.py +153 -0
  91. {reader-3.22 → reader-3.23}/tests/test_plugins_entry_dedupe.py +0 -1
  92. {reader-3.22 → reader-3.23}/tests/test_plugins_mark_as_read.py +0 -1
  93. {reader-3.22 → reader-3.23}/tests/test_plugins_preview_feed_list.py +0 -1
  94. {reader-3.22 → reader-3.23}/tests/test_plugins_readtime.py +0 -1
  95. {reader-3.22 → reader-3.23}/tests/test_reader_deprecations.py +0 -1
  96. {reader-3.22 → reader-3.23}/tests/test_reader_filter.py +25 -0
  97. {reader-3.22 → reader-3.23}/tests/test_reader_hooks.py +0 -1
  98. {reader-3.22 → reader-3.23}/tests/test_reader_lifecycle.py +0 -1
  99. {reader-3.22 → reader-3.23}/tests/test_reader_private.py +0 -1
  100. {reader-3.22 → reader-3.23}/tests/test_reader_search.py +2 -7
  101. {reader-3.22 → reader-3.23}/tests/test_reader_sort.py +0 -1
  102. {reader-3.22 → reader-3.23}/tests/test_reader_update.py +0 -1
  103. {reader-3.22 → reader-3.23}/tests/test_reader_utils.py +14 -2
  104. {reader-3.22 → reader-3.23}/tests/test_search.py +0 -1
  105. {reader-3.22 → reader-3.23}/tests/test_sql_utils.py +12 -22
  106. {reader-3.22 → reader-3.23}/tests/test_sqlite_utils.py +0 -1
  107. reader-3.22/docs/screenshots/feeds.png +0 -0
  108. {reader-3.22 → reader-3.23}/LICENSE +0 -0
  109. {reader-3.22 → reader-3.23}/MANIFEST.in +0 -0
  110. {reader-3.22 → reader-3.23}/docs/changelog.rst +0 -0
  111. {reader-3.22 → reader-3.23}/docs/cli.rst +0 -0
  112. {reader-3.22 → reader-3.23}/docs/config.rst +0 -0
  113. {reader-3.22 → reader-3.23}/docs/contributing.rst +0 -0
  114. {reader-3.22 → reader-3.23}/docs/index.rst +0 -0
  115. {reader-3.22 → reader-3.23}/docs/install.rst +0 -0
  116. {reader-3.22 → reader-3.23}/docs/internal.rst +0 -0
  117. {reader-3.22 → reader-3.23}/docs/plugins.rst +0 -0
  118. {reader-3.22 → reader-3.23}/docs/screenshots/entries-filters-light.png +0 -0
  119. {reader-3.22 → reader-3.23}/docs/screenshots/entries.png +0 -0
  120. {reader-3.22 → reader-3.23}/docs/screenshots/entry-image-light.png +0 -0
  121. {reader-3.22 → reader-3.23}/docs/screenshots/entry.png +0 -0
  122. {reader-3.22 → reader-3.23}/docs/screenshots/feed.png +0 -0
  123. {reader-3.22 → reader-3.23}/docs/tutorial.rst +0 -0
  124. {reader-3.22 → reader-3.23}/docs/why.rst +0 -0
  125. {reader-3.22 → reader-3.23}/examples/config.toml +0 -0
  126. {reader-3.22 → reader-3.23}/examples/custom_headers.py +0 -0
  127. {reader-3.22 → reader-3.23}/examples/feed_slugs.py +0 -0
  128. {reader-3.22 → reader-3.23}/examples/parser_only.py +0 -0
  129. {reader-3.22 → reader-3.23}/examples/terminal.py +0 -0
  130. {reader-3.22 → reader-3.23}/run.sh +0 -0
  131. {reader-3.22 → reader-3.23}/scripts/backup.sh +0 -0
  132. {reader-3.22 → reader-3.23}/scripts/jscontrols.html +0 -0
  133. {reader-3.22 → reader-3.23}/scripts/lines.sh +0 -0
  134. {reader-3.22 → reader-3.23}/scripts/release.py +0 -0
  135. {reader-3.22 → reader-3.23}/setup.cfg +0 -0
  136. {reader-3.22 → reader-3.23}/src/reader/_app/cli.py +0 -0
  137. {reader-3.22 → reader-3.23}/src/reader/_app/forms.py +0 -0
  138. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/api_thing.py +0 -0
  139. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/static/controls.js +0 -0
  140. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/static/style.css +0 -0
  141. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  142. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/entries.html +0 -0
  143. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/entry.html +0 -0
  144. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/feeds.html +0 -0
  145. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/layout.html +0 -0
  146. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/macros.html +0 -0
  147. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/metadata.html +0 -0
  148. {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/tags.html +0 -0
  149. {reader-3.22 → reader-3.23}/src/reader/_app/static/style.css +0 -0
  150. {reader-3.22 → reader-3.23}/src/reader/_app/static/theme.js +0 -0
  151. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/README +0 -0
  152. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  153. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  154. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  155. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  156. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  157. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  158. {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  159. {reader-3.22 → reader-3.23}/src/reader/_app/templates/change_feed_title.html +0 -0
  160. {reader-3.22 → reader-3.23}/src/reader/_app/templates/delete_feed.html +0 -0
  161. {reader-3.22 → reader-3.23}/src/reader/_app/templates/entries.html +0 -0
  162. {reader-3.22 → reader-3.23}/src/reader/_app/templates/entry.html +0 -0
  163. {reader-3.22 → reader-3.23}/src/reader/_app/templates/layout.html +0 -0
  164. {reader-3.22 → reader-3.23}/src/reader/_config_utils.py +0 -0
  165. {reader-3.22 → reader-3.23}/src/reader/_parser/_url_utils.py +0 -0
  166. {reader-3.22 → reader-3.23}/src/reader/_parser/file.py +0 -0
  167. {reader-3.22 → reader-3.23}/src/reader/_parser/http.py +0 -0
  168. {reader-3.22 → reader-3.23}/src/reader/_plugins/__init__.py +0 -0
  169. {reader-3.22 → reader-3.23}/src/reader/_plugins/cli_status.py +0 -0
  170. {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/__init__.py +0 -0
  171. {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  172. {reader-3.22 → reader-3.23}/src/reader/_plugins/timer.py +0 -0
  173. {reader-3.22 → reader-3.23}/src/reader/_vendor/__init__.py +0 -0
  174. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/__init__.py +0 -0
  175. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/api.py +0 -0
  176. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  177. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  178. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  179. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  180. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  181. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  182. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  183. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  184. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  185. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/encodings.py +0 -0
  186. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  187. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/html.py +0 -0
  188. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/http.py +0 -0
  189. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/mixin.py +0 -0
  190. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  191. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  192. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  193. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  194. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  195. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  196. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  197. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  198. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  199. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  200. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  201. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  202. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  203. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/py.typed +0 -0
  204. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  205. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/sgml.py +0 -0
  206. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/urls.py +0 -0
  207. {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/util.py +0 -0
  208. {reader-3.22 → reader-3.23}/src/reader/plugins/enclosure_dedupe.py +0 -0
  209. {reader-3.22 → reader-3.23}/src/reader/py.typed +0 -0
  210. {reader-3.22 → reader-3.23}/src/reader.egg-info/dependency_links.txt +0 -0
  211. {reader-3.22 → reader-3.23}/src/reader.egg-info/requires.txt +0 -0
  212. {reader-3.22 → reader-3.23}/src/reader.egg-info/top_level.txt +0 -0
  213. {reader-3.22 → reader-3.23}/tests/_plugins/__init__.py +0 -0
  214. {reader-3.22 → reader-3.23}/tests/_plugins/builtin.py +0 -0
  215. {reader-3.22 → reader-3.23}/tests/_plugins/error_during_import.py +0 -0
  216. {reader-3.22 → reader-3.23}/tests/_plugins/error_import_error.py +0 -0
  217. {reader-3.22 → reader-3.23}/tests/conftest.py +0 -0
  218. {reader-3.22 → reader-3.23}/tests/data/10.json +0 -0
  219. {reader-3.22 → reader-3.23}/tests/data/custom +0 -0
  220. {reader-3.22 → reader-3.23}/tests/data/empty.atom +0 -0
  221. {reader-3.22 → reader-3.23}/tests/data/empty.json +0 -0
  222. {reader-3.22 → reader-3.23}/tests/data/empty.rss +0 -0
  223. {reader-3.22 → reader-3.23}/tests/data/full.atom +0 -0
  224. {reader-3.22 → reader-3.23}/tests/data/full.json +0 -0
  225. {reader-3.22 → reader-3.23}/tests/data/full.rss +0 -0
  226. {reader-3.22 → reader-3.23}/tests/data/invalid.json +0 -0
  227. {reader-3.22 → reader-3.23}/tests/data/relative.atom +0 -0
  228. {reader-3.22 → reader-3.23}/tests/data/relative.rss +0 -0
  229. {reader-3.22 → reader-3.23}/tests/data/sqlite_releases.html +0 -0
  230. {reader-3.22 → reader-3.23}/tests/data/unknown.json +0 -0
  231. {reader-3.22 → reader-3.23}/tests/fakeparser.py +0 -0
  232. {reader-3.22 → reader-3.23}/tests/reader_methods.py +0 -0
  233. {reader-3.22 → reader-3.23}/tests/test_app_wsgi.py +0 -0
  234. {reader-3.22 → reader-3.23}/tests/test_cli.py +0 -0
  235. {reader-3.22 → reader-3.23}/tests/test_exceptions.py +0 -0
  236. {reader-3.22 → reader-3.23}/tests/test_hash_utils.py +0 -0
  237. {reader-3.22 → reader-3.23}/tests/test_parser.py +0 -0
  238. {reader-3.22 → reader-3.23}/tests/test_plugins_cli_status.py +0 -0
  239. {reader-3.22 → reader-3.23}/tests/test_plugins_enclosure_dedupe.py +0 -0
  240. {reader-3.22 → reader-3.23}/tests/test_plugins_loader.py +0 -0
  241. {reader-3.22 → reader-3.23}/tests/test_plugins_sqlite_releases.py +0 -0
  242. {reader-3.22 → reader-3.23}/tests/test_plugins_ua_fallback.py +0 -0
  243. {reader-3.22 → reader-3.23}/tests/test_reader.py +0 -0
  244. {reader-3.22 → reader-3.23}/tests/test_reader_counts.py +0 -0
  245. {reader-3.22 → reader-3.23}/tests/test_reader_integration.py +0 -0
  246. {reader-3.22 → reader-3.23}/tests/test_storage.py +0 -0
  247. {reader-3.22 → reader-3.23}/tests/test_tags.py +0 -0
  248. {reader-3.22 → reader-3.23}/tests/test_types.py +0 -0
  249. {reader-3.22 → reader-3.23}/tests/utils.py +0 -0
  250. {reader-3.22 → reader-3.23}/tox.ini +0 -0
@@ -6,6 +6,22 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.23
10
+ ------------
11
+
12
+ Released 2026-04-24
13
+
14
+ * Add support for :ref:`opml`. (:issue:`165`)
15
+
16
+ * Add support for import / export in the web app; :ref:`screenshots <screenshots-import>`.
17
+
18
+ * Deprecate the :func:`~reader.utils.archive_entries` ``feed_user_title`` argument.
19
+ * Fix :meth:`get_feeds(feed='') <Reader.get_feeds>` and
20
+ :meth:`get_entries(entry=('', '')) <Reader.get_entries>`
21
+ returning wrong entries.
22
+ (:issue:`392`)
23
+
24
+
9
25
  Version 3.22
10
26
  ------------
11
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.22
3
+ Version: 3.23
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -113,6 +113,7 @@ you'd probably call it a fat model.
113
113
  * filter feeds and articles
114
114
  * full-text search articles
115
115
  * get statistics on feed and user activity
116
+ * import / export feeds as OPML
116
117
  * write plugins to extend its functionality
117
118
  * skip all the low level stuff and focus on what makes your feed reader different
118
119
 
@@ -49,6 +49,7 @@ you'd probably call it a fat model.
49
49
  * filter feeds and articles
50
50
  * full-text search articles
51
51
  * get statistics on feed and user activity
52
+ * import / export feeds as OPML
52
53
  * write plugins to extend its functionality
53
54
  * skip all the low level stuff and focus on what makes your feed reader different
54
55
 
@@ -67,6 +67,17 @@ Data objects
67
67
  .. autoclass:: EntryUpdateStatus
68
68
  :members:
69
69
 
70
+ .. autoclass:: FeedToImport
71
+ :members:
72
+ :undoc-members:
73
+
74
+ .. autoclass:: FeedImportResult
75
+ :members:
76
+
77
+ .. autoclass:: FeedExport
78
+ :members:
79
+
80
+
70
81
 
71
82
  Exceptions
72
83
  ----------
@@ -155,6 +166,9 @@ Exceptions
155
166
  .. autoexception:: PluginInitError
156
167
  :show-inheritance:
157
168
 
169
+ .. autoexception:: FeedImportError
170
+ :show-inheritance:
171
+
158
172
 
159
173
  .. autoexception:: ReaderWarning
160
174
  :show-inheritance:
@@ -214,3 +228,10 @@ Utilities
214
228
  ---------
215
229
 
216
230
  .. autofunction:: reader.utils.archive_entries
231
+
232
+
233
+
234
+ ``reader.opml``
235
+ ---------------
236
+
237
+ .. automodule:: reader.opml
@@ -62,6 +62,18 @@ Screenshots
62
62
 
63
63
  article view (light mode)
64
64
 
65
+ .. _screenshots-import:
66
+
67
+ .. figure:: screenshots/import-select.png
68
+ :width: 240px
69
+
70
+ import feeds (select)
71
+
72
+ .. figure:: screenshots/import-result.png
73
+ :width: 240px
74
+
75
+ import feeds (result)
76
+
65
77
 
66
78
 
67
79
  Serving the web app
@@ -6,7 +6,6 @@ import unittest.mock
6
6
  import packaging.version
7
7
  from setuptools.config.pyprojecttoml import read_configuration
8
8
 
9
-
10
9
  sys.path.insert(0, os.path.abspath('../src'))
11
10
 
12
11
  # mock some things "by hand", so we can import reader below without any dependencies
@@ -25,7 +24,6 @@ for name in [
25
24
 
26
25
  import reader
27
26
 
28
-
29
27
  extensions = [
30
28
  'sphinx_rtd_theme',
31
29
  'sphinx.ext.autodoc',
@@ -32,7 +32,6 @@ in my spare time,
32
32
  but I will prioritize supporting :doc:`contributors <contributing>`
33
33
  (discussions, reviews and so on).
34
34
 
35
- * OPML support, :issue:`165`
36
35
  * :ref:`deleting entries <deleting entries>`
37
36
 
38
37
  * archiving important entries of deleted feeds, :issue:`290`
@@ -705,35 +705,32 @@ you can delete entries using the low-level
705
705
 
706
706
 
707
707
 
708
- .. _pagination:
708
+ .. _opml:
709
709
 
710
- Pagination
711
- ----------
710
+ OPML import / export
711
+ --------------------
712
712
 
713
- :meth:`~Reader.get_feeds`, :meth:`~Reader.get_entries`,
714
- and :meth:`~Reader.search_entries`
715
- can be used in a paginated fashion.
713
+ You can import and export feeds from and to an `OPML subscription list`_
714
+ by using the :meth:`~Reader.import_feeds`
715
+ and :meth:`~Reader.export_feeds` methods::
716
716
 
717
- The ``limit`` argument allows limiting the number of results returned;
718
- the ``starting_after`` argument allows skipping results until after
719
- a specific one.
720
-
721
- To get the first page, use only ``limit``::
722
-
723
- >>> for entry in reader.get_entries(limit=2):
724
- ... print(entry.title)
717
+ >>> export = reader.export_feeds()
718
+ >>> export.filename
719
+ 'reader-feeds-2026-04-24-10-00-00.opml'
720
+ >>> with open(export.filename, 'wb') as f:
721
+ ... f.write(export.content)
725
722
  ...
726
- H.I. #136: Dog Bingo
727
- H.I. #135: Place Your Bets
723
+ >>> # with another reader instance
724
+ >>> reader = make_reader(':memory:')
725
+ >>> with open(export.filename, 'rb') as f:
726
+ ... reader.import_feeds(f)
728
727
 
729
- To get the next page, use the last result from a call as
730
- ``starting_after`` in the next call::
728
+ For more detailed control over which feeds are imported,
729
+ see :meth:`~Reader.import_feeds_iter` and the :mod:`reader.opml` module.
731
730
 
732
- >>> for entry in reader.get_entries(limit=2, starting_after=entry):
733
- ... print(entry.title)
734
- ...
735
- # H.I. 134: Boxing Day
736
- Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
731
+ .. _OPML subscription list: https://opml.org/spec2.opml#subscriptionLists
732
+
733
+ .. versionadded:: 3.23
737
734
 
738
735
 
739
736
 
@@ -831,6 +828,38 @@ Some examples of how this is useful:
831
828
 
832
829
 
833
830
 
831
+ .. _pagination:
832
+
833
+ Pagination
834
+ ----------
835
+
836
+ :meth:`~Reader.get_feeds`, :meth:`~Reader.get_entries`,
837
+ and :meth:`~Reader.search_entries`
838
+ can be used in a paginated fashion.
839
+
840
+ The ``limit`` argument allows limiting the number of results returned;
841
+ the ``starting_after`` argument allows skipping results until after
842
+ a specific one.
843
+
844
+ To get the first page, use only ``limit``::
845
+
846
+ >>> for entry in reader.get_entries(limit=2):
847
+ ... print(entry.title)
848
+ ...
849
+ H.I. #136: Dog Bingo
850
+ H.I. #135: Place Your Bets
851
+
852
+ To get the next page, use the last result from a call as
853
+ ``starting_after`` in the next call::
854
+
855
+ >>> for entry in reader.get_entries(limit=2, starting_after=entry):
856
+ ... print(entry.title)
857
+ ...
858
+ # H.I. 134: Boxing Day
859
+ Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
860
+
861
+
862
+
834
863
  .. _reserved names:
835
864
 
836
865
  Reserved names
Binary file
@@ -14,7 +14,6 @@ import requests
14
14
 
15
15
  from reader import make_reader
16
16
 
17
-
18
17
  feed_url = "http://www.hellointernet.fm/podcast?format=rss"
19
18
  podcasts_dir = "podcasts"
20
19
 
@@ -199,4 +199,3 @@ skip_gitignore = true
199
199
  filter_files = true
200
200
  force_single_line = true
201
201
  order_by_type = false
202
- lines_after_imports = 2
@@ -14,7 +14,6 @@ from functools import partial
14
14
 
15
15
  import click
16
16
 
17
-
18
17
  root_dir = os.path.dirname(__file__)
19
18
  sys.path.insert(0, os.path.join(root_dir, '../src'))
20
19
  sys.path.insert(0, os.path.join(root_dir, '../tests'))
@@ -29,7 +29,6 @@ import click
29
29
  import numpy as np
30
30
  from tabulate import tabulate
31
31
 
32
-
33
32
  # assumes dict()s are ordered
34
33
 
35
34
  value_funcs = {
@@ -21,7 +21,6 @@ import time
21
21
  from reader import make_reader
22
22
  from reader.plugins import entry_dedupe
23
23
 
24
-
25
24
  DB_ARCHIVE = "_backups/reader.sqlite.2025-10-04.gz"
26
25
 
27
26
  file = sys.argv[1]
@@ -8,14 +8,12 @@ from flask import jsonify
8
8
  from flask import redirect
9
9
  from flask import request
10
10
 
11
-
12
11
  root_dir = os.path.dirname(__file__)
13
12
  sys.path.insert(0, os.path.join(root_dir, '../src'))
14
13
 
15
14
  from reader._app.api_thing import APIError
16
15
  from reader._app.api_thing import APIThing
17
16
 
18
-
19
17
  app = Flask(
20
18
  __name__,
21
19
  template_folder='../src/reader/_app/templates',
@@ -3,7 +3,6 @@ import shutil
3
3
 
4
4
  import requests
5
5
 
6
-
7
6
  STUFF = {
8
7
  "htmx.org@2.0.8": ["dist/htmx.min.js"],
9
8
  "htmx-ext-response-targets@2.0.4": ["dist/response-targets.min.js"],
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.22'
40
+ __version__ = '3.23'
41
41
 
42
42
  # isort: off
43
43
 
@@ -64,6 +64,9 @@ from .types import (
64
64
  UpdateResult as UpdateResult,
65
65
  UpdatedFeed as UpdatedFeed,
66
66
  EntryUpdateStatus as EntryUpdateStatus,
67
+ FeedToImport as FeedToImport,
68
+ FeedImportResult as FeedImportResult,
69
+ FeedExport as FeedExport,
67
70
  )
68
71
 
69
72
  from .exceptions import (
@@ -91,12 +94,13 @@ from .exceptions import (
91
94
  InvalidPluginError as InvalidPluginError,
92
95
  PluginInitError as PluginInitError,
93
96
  ReaderWarning as ReaderWarning,
97
+ FeedImportError as FeedImportError,
94
98
  )
95
99
 
96
-
97
100
  # Constants.
98
101
 
99
- USER_AGENT = f'python-reader/{__version__} (+https://github.com/lemon24/reader)'
102
+ SOURCE_URL = 'https://github.com/lemon24/reader'
103
+ USER_AGENT = f'python-reader/{__version__} (+{SOURCE_URL})'
100
104
 
101
105
 
102
106
  # Prevent any logging output by default. If no handler is set,
@@ -1,6 +1,5 @@
1
1
  import sys
2
2
 
3
-
4
3
  CANNOT_IMPORT = """\
5
4
  Error: cannot import reader._cli
6
5
 
@@ -1,4 +1,6 @@
1
+ import json
1
2
  import pathlib
3
+ from dataclasses import asdict
2
4
  from datetime import datetime
3
5
  from datetime import timedelta
4
6
  from datetime import timezone
@@ -28,7 +30,9 @@ from werkzeug.exceptions import NotFound
28
30
  from reader import EntryNotFoundError
29
31
  from reader import FeedExistsError
30
32
  from reader import FeedNotFoundError
33
+ from reader import FeedToImport
31
34
  from reader import InvalidFeedURLError
35
+ from reader import opml
32
36
  from reader import UpdateError
33
37
 
34
38
  from .ext import get_reader
@@ -38,7 +42,6 @@ from .forms import ChangeFeedTitle
38
42
  from .forms import EntryFilter
39
43
  from .forms import FeedFilter
40
44
 
41
-
42
45
  # for a prototype with tags and search support, see
43
46
  # https://github.com/lemon24/reader/tree/3.21/src/reader/_app/v2
44
47
 
@@ -164,6 +167,10 @@ def feeds():
164
167
  if form.validate():
165
168
  feeds = reader.get_feeds(**kwargs)
166
169
 
170
+ if request.args.get('format', '').lower() == 'opml':
171
+ export = reader.export_feeds(feeds)
172
+ return export.content, export.headers
173
+
167
174
  return stream_template(
168
175
  'feeds.html',
169
176
  form=form,
@@ -270,6 +277,34 @@ def add_feed():
270
277
  return render_template('add_feed.html', form=form)
271
278
 
272
279
 
280
+ @blueprint.route('/feeds/import', methods=['GET', 'POST'])
281
+ def import_feeds():
282
+ reader = get_reader()
283
+ parsed_feeds = None
284
+ error = None
285
+ imported_feeds = None
286
+
287
+ if request.method == 'POST':
288
+ if file := request.files.get('file'):
289
+ try:
290
+ parsed_feeds = [asdict(f) for f in opml.parse(file.stream)]
291
+ except opml.OPMLError as e:
292
+ error = str(e)
293
+ else:
294
+ feeds = (
295
+ FeedToImport(**json.loads(f)) for f in request.form.getlist('feed')
296
+ )
297
+ imported_feeds = list(reader.import_feeds_iter(feeds))
298
+ # TODO: out of band update (unlike add, there's too many to do here)
299
+
300
+ return render_template(
301
+ 'import_feeds.html',
302
+ parsed_feeds=parsed_feeds,
303
+ error=error,
304
+ imported_feeds=imported_feeds,
305
+ )
306
+
307
+
273
308
  @blueprint.route('/entry')
274
309
  def entry():
275
310
  reader = get_reader()
@@ -10,7 +10,6 @@ from flask import current_app
10
10
  from reader import make_reader
11
11
  from reader.plugins._loader import PluginLoader
12
12
 
13
-
14
13
  _plugin_loader = PluginLoader('init_app')
15
14
 
16
15
 
@@ -44,7 +44,6 @@ from ..ext import ReaderExtension
44
44
  from .api_thing import APIError
45
45
  from .api_thing import APIThing
46
46
 
47
-
48
47
  blueprint = Blueprint(
49
48
  'reader', __name__, static_folder='static', template_folder='templates'
50
49
  )
@@ -15,7 +15,6 @@ import os
15
15
  import reader._app.legacy
16
16
  import reader._cli
17
17
 
18
-
19
18
  config = reader._cli.load_reader_config()
20
19
  app = reader._app.legacy.create_app(config)
21
20
  app.config['TRAP_BAD_REQUEST_ERRORS'] = bool(
@@ -12,7 +12,9 @@
12
12
 
13
13
  <h1>Add feed</h1>
14
14
 
15
- <form action="{{ url_for(request.endpoint) }}" method="post" class="my-2">
15
+ <p>Add a single feed:</p>
16
+
17
+ <form action="{{ url_for(request.endpoint) }}" method="post" class="my-2 mb-4">
16
18
  {#- TODO: aria stuff #}
17
19
 
18
20
  {{ form.csrf_token }}
@@ -43,4 +45,10 @@
43
45
 
44
46
  </form>
45
47
 
48
+ <p>
49
+ ...or
50
+ <a href="{{ url_for('.import_feeds') }}">import feeds</a>
51
+ from an OPML subscription list.
52
+ </p>
53
+
46
54
  {% endblock %}
@@ -12,7 +12,14 @@
12
12
 
13
13
  {{ macros.flashed_messages() }}
14
14
 
15
- <h1>Feeds</h1>
15
+ <h1>
16
+ <span class="me-1">
17
+ Feeds
18
+ </span>
19
+ <a href="{{ url_for(request.endpoint, format='opml', **request.args) }}" class="small" title="export as OPML">
20
+ <i class="bi bi-filetype-xml text-secondary"></i>
21
+ </a>
22
+ </h1>
16
23
 
17
24
 
18
25
  {% call macros.filter_form(form) %}
@@ -0,0 +1,121 @@
1
+ {% extends "layout.html" %}
2
+
3
+ {% import "macros.html" as macros %}
4
+
5
+
6
+ {% block page_title %}
7
+ Import feeds - {{ super() }}
8
+ {% endblock %}
9
+
10
+
11
+ {% block body %}
12
+
13
+ <h1>Import feeds</h1>
14
+
15
+ <form
16
+ action="{{ url_for(request.endpoint) }}"
17
+ method="post"
18
+ enctype="multipart/form-data"
19
+ class="my-2 mb-4"
20
+ >
21
+ {#- TODO: aria stuff #}
22
+
23
+ {{ macros.csrf_token_field() }}
24
+
25
+ {% if parsed_feeds is none and imported_feeds is none %}
26
+
27
+ <p>Upload OPML subscription list:</p>
28
+
29
+ <div class="mb-3">
30
+
31
+ <!-- TODO accept -->
32
+ <input
33
+ class="form-control{% if error %} is-invalid {% endif %}"
34
+ type="file"
35
+ name="file"
36
+ >
37
+ {%- if error %}
38
+ <div class="invalid-feedback">
39
+ {{ error }}
40
+ </div>
41
+ {%- endif %}
42
+
43
+ </div>
44
+
45
+ <input type="submit" class="btn btn-primary" value="upload file">
46
+
47
+ <a href="javascript:history.back()" class="btn btn-outline-secondary" role="button" >
48
+ cancel
49
+ </a>
50
+
51
+ {% elif parsed_feeds is not none %}
52
+
53
+ <p>Select feeds to import:</p>
54
+
55
+ {% for feed in parsed_feeds %}
56
+ <div class="form-check mb-3">
57
+ <input
58
+ class="form-check-input"
59
+ id="feed-{{ loop.index }}"
60
+ type="checkbox"
61
+ name="feed"
62
+ value='{{ feed | tojson }}'
63
+ checked
64
+ >
65
+
66
+ <label class="form-check-label" for="feed-{{ loop.index }}">
67
+ {#- NOTE similarity to feeds page feeds #}
68
+ <h2 class="h5 mb-1" style="font-size: 1.125rem">
69
+ {{ macros.title_href(
70
+ feed.title or feed.url,
71
+ None,
72
+ external=feed.link,
73
+ feed_url=feed.url,
74
+ ) }}
75
+ </h2>
76
+ {{ macros.subtitle(feed.subtitle) }}
77
+ </label>
78
+
79
+ </div>
80
+ {% endfor %}
81
+
82
+ <input type="submit" class="btn btn-primary" value="import selected">
83
+
84
+ <a href="javascript:history.back()" class="btn btn-outline-secondary" role="button" >
85
+ cancel
86
+ </a>
87
+
88
+ {% else %}
89
+
90
+ <p>Imported feeds:</p>
91
+
92
+ {% for result in imported_feeds %}
93
+ {%- set feed = result.feed %}
94
+ <div class="mb-3">
95
+
96
+ {#- NOTE similarity to feeds page feeds #}
97
+ <h2 class="h5 mb-1" style="font-size: 1.125rem">
98
+ {{ macros.title_href(
99
+ feed.title or feed.url,
100
+ url_for('.entries', feed=feed.url) if not result.error else None,
101
+ external=feed.link,
102
+ feed_url=feed.url,
103
+ ) }}
104
+ </h2>
105
+
106
+ {% if result.added %}
107
+ {{ macros.subtitle("feed added", 'text-success-emphasis') }}
108
+ {% elif result.error %}
109
+ {{ macros.subtitle("invalid feed: {}".format(result.error), 'text-danger-emphasis') }}
110
+ {% else %}
111
+ {{ macros.subtitle("feed already exists", 'text-secondary') }}
112
+ {% endif %}
113
+
114
+ </div>
115
+ {% endfor %}
116
+
117
+ {% endif %}
118
+
119
+ </form>
120
+
121
+ {% endblock %}
@@ -1,6 +1,8 @@
1
1
  <!doctype html>
2
2
  <html lang="en" data-bs-theme="auto">
3
3
  <head>
4
+ {% block head %}
5
+
4
6
  <meta charset="utf-8">
5
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
8
 
@@ -18,6 +20,7 @@
18
20
  <script src="{{ find_static('htmx.min.js', 'reader') }}" defer></script>
19
21
  <script src="{{ find_static('response-targets.min.js', 'reader') }}" defer></script>
20
22
 
23
+ {% endblock %}
21
24
  </head>
22
25
  <body hx-ext="response-targets">
23
26
 
@@ -245,7 +245,7 @@
245
245
  {% endmacro %}
246
246
 
247
247
 
248
- {% macro title_href(text, href=none, external=none) %}
248
+ {% macro title_href(text, href=none, external=none, feed_url=none) %}
249
249
  {%- set text = text | truncate(95) -%}
250
250
 
251
251
  {%- macro maybe_href(text, href) %}
@@ -254,7 +254,7 @@
254
254
  {%- if href %}</a>{% endif -%}
255
255
  {%- endmacro %}
256
256
 
257
- {%- if not external %}
257
+ {%- if not (external or feed_url) %}
258
258
  {{ maybe_href(text, href) }}
259
259
  {%- else %}
260
260
  {%- set first, _, last = text.strip().rpartition(' ') -%}
@@ -263,19 +263,34 @@
263
263
  <span class="me-1">
264
264
  {{ maybe_href(last, href) }}
265
265
  </span>
266
- <a href="{{ external }}" target="_blank" class="text-nowrap small" title="external">
267
- <i class="bi bi-box-arrow-up-right text-secondary"></i>
268
- </a>
266
+ {% if external %}
267
+ <a
268
+ href="{{ external }}"
269
+ target="_blank"
270
+ class="text-nowrap small{% if feed_url %} me-1{% endif %}"
271
+ title="external"
272
+ >
273
+ <i class="bi bi-box-arrow-up-right text-secondary"></i></a>
274
+ {% endif %}
275
+ {% if feed_url %}
276
+ <a
277
+ href="{{ feed_url }}"
278
+ target="_blank"
279
+ class="text-nowrap small"
280
+ title="feed source"
281
+ >
282
+ <i class="bi bi-rss text-secondary"></i></a>
283
+ {% endif %}
269
284
  </span>
270
285
  {%- endif %}
271
286
 
272
287
  {%- endmacro %}
273
288
 
274
289
 
275
- {% macro subtitle(text) %}
290
+ {% macro subtitle(text, class=none) %}
276
291
  {%- set text = (text or '') | striptags | trim -%}
277
292
  {%- if text %}
278
- <p class="small mb-2">
293
+ <p class="small mb-1{% if class %} {{class}}{% endif %}">
279
294
  {{ text | truncate(223) }}
280
295
  </p>
281
296
  {%- endif %}