reader 3.25__tar.gz → 3.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. {reader-3.25 → reader-3.26}/CHANGES.rst +14 -1
  2. {reader-3.25/src/reader.egg-info → reader-3.26}/PKG-INFO +3 -1
  3. {reader-3.25 → reader-3.26}/docs/app.rst +16 -0
  4. {reader-3.25 → reader-3.26}/docs/guide.rst +10 -7
  5. reader-3.26/docs/screenshots/autodiscovery.png +0 -0
  6. reader-3.26/docs/screenshots/export-database.png +0 -0
  7. {reader-3.25 → reader-3.26}/pyproject.toml +4 -1
  8. {reader-3.25 → reader-3.26}/src/reader/__init__.py +1 -1
  9. {reader-3.25 → reader-3.26}/src/reader/_app/__init__.py +37 -1
  10. {reader-3.25 → reader-3.26}/src/reader/_app/cli.py +12 -1
  11. reader-3.26/src/reader/_app/exports.py +58 -0
  12. {reader-3.25 → reader-3.26}/src/reader/_app/ext.py +29 -2
  13. {reader-3.25 → reader-3.26}/src/reader/_app/static/style.css +2 -4
  14. {reader-3.25 → reader-3.26}/src/reader/_app/templates/entries.html +1 -1
  15. {reader-3.25 → reader-3.26}/src/reader/_app/templates/layout_base.html +17 -1
  16. {reader-3.25 → reader-3.26}/src/reader/_app/templates/macros.html +5 -4
  17. reader-3.26/src/reader/_app/templates/settings.html +1 -0
  18. reader-3.26/src/reader/_app/templates/settings_base.html +67 -0
  19. {reader-3.25 → reader-3.26}/src/reader/_cli.py +12 -0
  20. {reader-3.25 → reader-3.26}/src/reader/_storage/_base.py +31 -1
  21. {reader-3.25 → reader-3.26}/src/reader/_types.py +14 -1
  22. {reader-3.25 → reader-3.26/src/reader.egg-info}/PKG-INFO +3 -1
  23. {reader-3.25 → reader-3.26}/src/reader.egg-info/SOURCES.txt +5 -1
  24. {reader-3.25 → reader-3.26}/src/reader.egg-info/requires.txt +2 -0
  25. {reader-3.25 → reader-3.26}/tests/test_storage.py +16 -0
  26. reader-3.25/scripts/backup.sh +0 -36
  27. {reader-3.25 → reader-3.26}/LICENSE +0 -0
  28. {reader-3.25 → reader-3.26}/MANIFEST.in +0 -0
  29. {reader-3.25 → reader-3.26}/README.rst +0 -0
  30. {reader-3.25 → reader-3.26}/docs/api.rst +0 -0
  31. {reader-3.25 → reader-3.26}/docs/changelog.rst +0 -0
  32. {reader-3.25 → reader-3.26}/docs/cli.rst +0 -0
  33. {reader-3.25 → reader-3.26}/docs/conf.py +0 -0
  34. {reader-3.25 → reader-3.26}/docs/config.rst +0 -0
  35. {reader-3.25 → reader-3.26}/docs/contributing.rst +0 -0
  36. {reader-3.25 → reader-3.26}/docs/dev.rst +0 -0
  37. {reader-3.25 → reader-3.26}/docs/index.rst +0 -0
  38. {reader-3.25 → reader-3.26}/docs/install.rst +0 -0
  39. {reader-3.25 → reader-3.26}/docs/internal.rst +0 -0
  40. {reader-3.25 → reader-3.26}/docs/plugins.rst +0 -0
  41. {reader-3.25 → reader-3.26}/docs/screenshots/empty.png +0 -0
  42. {reader-3.25 → reader-3.26}/docs/screenshots/entries-filters-light.png +0 -0
  43. {reader-3.25 → reader-3.26}/docs/screenshots/entries.png +0 -0
  44. {reader-3.25 → reader-3.26}/docs/screenshots/entry-image-light.png +0 -0
  45. {reader-3.25 → reader-3.26}/docs/screenshots/entry.png +0 -0
  46. {reader-3.25 → reader-3.26}/docs/screenshots/feed.png +0 -0
  47. {reader-3.25 → reader-3.26}/docs/screenshots/feeds.png +0 -0
  48. {reader-3.25 → reader-3.26}/docs/screenshots/help.png +0 -0
  49. {reader-3.25 → reader-3.26}/docs/screenshots/import-result.png +0 -0
  50. {reader-3.25 → reader-3.26}/docs/screenshots/import-select.png +0 -0
  51. {reader-3.25 → reader-3.26}/docs/tutorial.rst +0 -0
  52. {reader-3.25 → reader-3.26}/docs/why.rst +0 -0
  53. {reader-3.25 → reader-3.26}/examples/config.toml +0 -0
  54. {reader-3.25 → reader-3.26}/examples/custom_headers.py +0 -0
  55. {reader-3.25 → reader-3.26}/examples/feed_slugs.py +0 -0
  56. {reader-3.25 → reader-3.26}/examples/parser_only.py +0 -0
  57. {reader-3.25 → reader-3.26}/examples/podcast.py +0 -0
  58. {reader-3.25 → reader-3.26}/examples/terminal.py +0 -0
  59. {reader-3.25 → reader-3.26}/run.sh +0 -0
  60. {reader-3.25 → reader-3.26}/scripts/bench.py +0 -0
  61. {reader-3.25 → reader-3.26}/scripts/debug_storage_stats.py +0 -0
  62. {reader-3.25 → reader-3.26}/scripts/entry_dedupe_backtest.py +0 -0
  63. {reader-3.25 → reader-3.26}/scripts/jscontrols.html +0 -0
  64. {reader-3.25 → reader-3.26}/scripts/jscontrols.py +0 -0
  65. {reader-3.25 → reader-3.26}/scripts/lines.sh +0 -0
  66. {reader-3.25 → reader-3.26}/scripts/release.py +0 -0
  67. {reader-3.25 → reader-3.26}/scripts/vendor_app_stuff.py +0 -0
  68. {reader-3.25 → reader-3.26}/setup.cfg +0 -0
  69. {reader-3.25 → reader-3.26}/src/reader/__main__.py +0 -0
  70. {reader-3.25 → reader-3.26}/src/reader/_app/forms.py +0 -0
  71. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/__init__.py +0 -0
  72. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/api_thing.py +0 -0
  73. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/static/controls.js +0 -0
  74. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/static/style.css +0 -0
  75. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  76. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/entries.html +0 -0
  77. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/entry.html +0 -0
  78. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/feeds.html +0 -0
  79. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/layout.html +0 -0
  80. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/macros.html +0 -0
  81. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/metadata.html +0 -0
  82. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/templates/tags.html +0 -0
  83. {reader-3.25 → reader-3.26}/src/reader/_app/legacy/wsgi.py +0 -0
  84. {reader-3.25 → reader-3.26}/src/reader/_app/static/theme.js +0 -0
  85. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/README +0 -0
  86. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  87. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  88. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  89. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  90. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  91. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  92. {reader-3.25 → reader-3.26}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  93. {reader-3.25 → reader-3.26}/src/reader/_app/templates/add_feed.html +0 -0
  94. {reader-3.25 → reader-3.26}/src/reader/_app/templates/change_feed_title.html +0 -0
  95. {reader-3.25 → reader-3.26}/src/reader/_app/templates/delete_feed.html +0 -0
  96. {reader-3.25 → reader-3.26}/src/reader/_app/templates/entry.html +0 -0
  97. {reader-3.25 → reader-3.26}/src/reader/_app/templates/feeds.html +0 -0
  98. {reader-3.25 → reader-3.26}/src/reader/_app/templates/help.html +0 -0
  99. {reader-3.25 → reader-3.26}/src/reader/_app/templates/import_feeds.html +0 -0
  100. {reader-3.25 → reader-3.26}/src/reader/_app/templates/layout.html +0 -0
  101. {reader-3.25 → reader-3.26}/src/reader/_app/wsgi.py +0 -0
  102. {reader-3.25 → reader-3.26}/src/reader/_config_utils.py +0 -0
  103. {reader-3.25 → reader-3.26}/src/reader/_hash_utils.py +0 -0
  104. {reader-3.25 → reader-3.26}/src/reader/_logging.py +0 -0
  105. {reader-3.25 → reader-3.26}/src/reader/_parser/__init__.py +0 -0
  106. {reader-3.25 → reader-3.26}/src/reader/_parser/_http_utils.py +0 -0
  107. {reader-3.25 → reader-3.26}/src/reader/_parser/_url_utils.py +0 -0
  108. {reader-3.25 → reader-3.26}/src/reader/_parser/feedparser.py +0 -0
  109. {reader-3.25 → reader-3.26}/src/reader/_parser/file.py +0 -0
  110. {reader-3.25 → reader-3.26}/src/reader/_parser/http.py +0 -0
  111. {reader-3.25 → reader-3.26}/src/reader/_parser/jsonfeed.py +0 -0
  112. {reader-3.25 → reader-3.26}/src/reader/_plugins/__init__.py +0 -0
  113. {reader-3.25 → reader-3.26}/src/reader/_plugins/cli_status.py +0 -0
  114. {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/__init__.py +0 -0
  115. {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
  116. {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/preview_feed_list.py +0 -0
  117. {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/share.py +0 -0
  118. {reader-3.25 → reader-3.26}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  119. {reader-3.25 → reader-3.26}/src/reader/_plugins/sqlite_releases.py +0 -0
  120. {reader-3.25 → reader-3.26}/src/reader/_plugins/timer.py +0 -0
  121. {reader-3.25 → reader-3.26}/src/reader/_storage/__init__.py +0 -0
  122. {reader-3.25 → reader-3.26}/src/reader/_storage/_changes.py +0 -0
  123. {reader-3.25 → reader-3.26}/src/reader/_storage/_entries.py +0 -0
  124. {reader-3.25 → reader-3.26}/src/reader/_storage/_feeds.py +0 -0
  125. {reader-3.25 → reader-3.26}/src/reader/_storage/_html_utils.py +0 -0
  126. {reader-3.25 → reader-3.26}/src/reader/_storage/_schema.py +0 -0
  127. {reader-3.25 → reader-3.26}/src/reader/_storage/_search.py +0 -0
  128. {reader-3.25 → reader-3.26}/src/reader/_storage/_sql_utils.py +0 -0
  129. {reader-3.25 → reader-3.26}/src/reader/_storage/_sqlite_utils.py +0 -0
  130. {reader-3.25 → reader-3.26}/src/reader/_storage/_tags.py +0 -0
  131. {reader-3.25 → reader-3.26}/src/reader/_update/__init__.py +0 -0
  132. {reader-3.25 → reader-3.26}/src/reader/_update/base.py +0 -0
  133. {reader-3.25 → reader-3.26}/src/reader/_update/hooks.py +0 -0
  134. {reader-3.25 → reader-3.26}/src/reader/_utils.py +0 -0
  135. {reader-3.25 → reader-3.26}/src/reader/_vendor/__init__.py +0 -0
  136. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/__init__.py +0 -0
  137. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/api.py +0 -0
  138. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  139. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  140. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  141. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  142. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  143. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  144. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  145. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  146. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  147. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/encodings.py +0 -0
  148. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  149. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/html.py +0 -0
  150. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/http.py +0 -0
  151. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/mixin.py +0 -0
  152. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  153. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  154. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  155. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  156. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  157. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  158. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  159. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  160. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  161. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  162. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  163. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  164. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  165. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/py.typed +0 -0
  166. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  167. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/sgml.py +0 -0
  168. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/urls.py +0 -0
  169. {reader-3.25 → reader-3.26}/src/reader/_vendor/feedparser/util.py +0 -0
  170. {reader-3.25 → reader-3.26}/src/reader/core.py +0 -0
  171. {reader-3.25 → reader-3.26}/src/reader/discover.py +0 -0
  172. {reader-3.25 → reader-3.26}/src/reader/exceptions.py +0 -0
  173. {reader-3.25 → reader-3.26}/src/reader/opml.py +0 -0
  174. {reader-3.25 → reader-3.26}/src/reader/plugins/__init__.py +0 -0
  175. {reader-3.25 → reader-3.26}/src/reader/plugins/_loader.py +0 -0
  176. {reader-3.25 → reader-3.26}/src/reader/plugins/autodiscover.py +0 -0
  177. {reader-3.25 → reader-3.26}/src/reader/plugins/enclosure_dedupe.py +0 -0
  178. {reader-3.25 → reader-3.26}/src/reader/plugins/entry_dedupe.py +0 -0
  179. {reader-3.25 → reader-3.26}/src/reader/plugins/mark_as_read.py +0 -0
  180. {reader-3.25 → reader-3.26}/src/reader/plugins/readtime.py +0 -0
  181. {reader-3.25 → reader-3.26}/src/reader/plugins/ua_fallback.py +0 -0
  182. {reader-3.25 → reader-3.26}/src/reader/py.typed +0 -0
  183. {reader-3.25 → reader-3.26}/src/reader/types.py +0 -0
  184. {reader-3.25 → reader-3.26}/src/reader/utils.py +0 -0
  185. {reader-3.25 → reader-3.26}/src/reader.egg-info/dependency_links.txt +0 -0
  186. {reader-3.25 → reader-3.26}/src/reader.egg-info/top_level.txt +0 -0
  187. {reader-3.25 → reader-3.26}/tests/_plugins/__init__.py +0 -0
  188. {reader-3.25 → reader-3.26}/tests/_plugins/builtin.py +0 -0
  189. {reader-3.25 → reader-3.26}/tests/_plugins/error_during_import.py +0 -0
  190. {reader-3.25 → reader-3.26}/tests/_plugins/error_import_error.py +0 -0
  191. {reader-3.25 → reader-3.26}/tests/conftest.py +0 -0
  192. {reader-3.25 → reader-3.26}/tests/data/10.json +0 -0
  193. {reader-3.25 → reader-3.26}/tests/data/10.json.py +0 -0
  194. {reader-3.25 → reader-3.26}/tests/data/custom +0 -0
  195. {reader-3.25 → reader-3.26}/tests/data/empty.atom +0 -0
  196. {reader-3.25 → reader-3.26}/tests/data/empty.atom.py +0 -0
  197. {reader-3.25 → reader-3.26}/tests/data/empty.json +0 -0
  198. {reader-3.25 → reader-3.26}/tests/data/empty.json.py +0 -0
  199. {reader-3.25 → reader-3.26}/tests/data/empty.rss +0 -0
  200. {reader-3.25 → reader-3.26}/tests/data/empty.rss.py +0 -0
  201. {reader-3.25 → reader-3.26}/tests/data/full.atom +0 -0
  202. {reader-3.25 → reader-3.26}/tests/data/full.atom.py +0 -0
  203. {reader-3.25 → reader-3.26}/tests/data/full.json +0 -0
  204. {reader-3.25 → reader-3.26}/tests/data/full.json.py +0 -0
  205. {reader-3.25 → reader-3.26}/tests/data/full.rss +0 -0
  206. {reader-3.25 → reader-3.26}/tests/data/full.rss.py +0 -0
  207. {reader-3.25 → reader-3.26}/tests/data/invalid.json +0 -0
  208. {reader-3.25 → reader-3.26}/tests/data/invalid.json.py +0 -0
  209. {reader-3.25 → reader-3.26}/tests/data/relative.atom +0 -0
  210. {reader-3.25 → reader-3.26}/tests/data/relative.atom.py +0 -0
  211. {reader-3.25 → reader-3.26}/tests/data/relative.rss +0 -0
  212. {reader-3.25 → reader-3.26}/tests/data/relative.rss.py +0 -0
  213. {reader-3.25 → reader-3.26}/tests/data/sqlite_releases.html +0 -0
  214. {reader-3.25 → reader-3.26}/tests/data/unknown.json +0 -0
  215. {reader-3.25 → reader-3.26}/tests/data/unknown.json.py +0 -0
  216. {reader-3.25 → reader-3.26}/tests/fakeparser.py +0 -0
  217. {reader-3.25 → reader-3.26}/tests/fixtures.py +0 -0
  218. {reader-3.25 → reader-3.26}/tests/reader_methods.py +0 -0
  219. {reader-3.25 → reader-3.26}/tests/test__types.py +0 -0
  220. {reader-3.25 → reader-3.26}/tests/test__utils.py +0 -0
  221. {reader-3.25 → reader-3.26}/tests/test_app_legacy.py +0 -0
  222. {reader-3.25 → reader-3.26}/tests/test_app_wsgi.py +0 -0
  223. {reader-3.25 → reader-3.26}/tests/test_bench.py +0 -0
  224. {reader-3.25 → reader-3.26}/tests/test_changes.py +0 -0
  225. {reader-3.25 → reader-3.26}/tests/test_cli.py +0 -0
  226. {reader-3.25 → reader-3.26}/tests/test_config_utils.py +0 -0
  227. {reader-3.25 → reader-3.26}/tests/test_discover.py +0 -0
  228. {reader-3.25 → reader-3.26}/tests/test_exceptions.py +0 -0
  229. {reader-3.25 → reader-3.26}/tests/test_hash_utils.py +0 -0
  230. {reader-3.25 → reader-3.26}/tests/test_html_utils.py +0 -0
  231. {reader-3.25 → reader-3.26}/tests/test_lazy_imports.py +0 -0
  232. {reader-3.25 → reader-3.26}/tests/test_logging.py +0 -0
  233. {reader-3.25 → reader-3.26}/tests/test_opml.py +0 -0
  234. {reader-3.25 → reader-3.26}/tests/test_parser.py +0 -0
  235. {reader-3.25 → reader-3.26}/tests/test_plugins_autodiscover.py +0 -0
  236. {reader-3.25 → reader-3.26}/tests/test_plugins_cli_status.py +0 -0
  237. {reader-3.25 → reader-3.26}/tests/test_plugins_enclosure_dedupe.py +0 -0
  238. {reader-3.25 → reader-3.26}/tests/test_plugins_entry_dedupe.py +0 -0
  239. {reader-3.25 → reader-3.26}/tests/test_plugins_loader.py +0 -0
  240. {reader-3.25 → reader-3.26}/tests/test_plugins_mark_as_read.py +0 -0
  241. {reader-3.25 → reader-3.26}/tests/test_plugins_preview_feed_list.py +0 -0
  242. {reader-3.25 → reader-3.26}/tests/test_plugins_readtime.py +0 -0
  243. {reader-3.25 → reader-3.26}/tests/test_plugins_sqlite_releases.py +0 -0
  244. {reader-3.25 → reader-3.26}/tests/test_plugins_ua_fallback.py +0 -0
  245. {reader-3.25 → reader-3.26}/tests/test_reader.py +0 -0
  246. {reader-3.25 → reader-3.26}/tests/test_reader_counts.py +0 -0
  247. {reader-3.25 → reader-3.26}/tests/test_reader_deprecations.py +0 -0
  248. {reader-3.25 → reader-3.26}/tests/test_reader_filter.py +0 -0
  249. {reader-3.25 → reader-3.26}/tests/test_reader_hooks.py +0 -0
  250. {reader-3.25 → reader-3.26}/tests/test_reader_integration.py +0 -0
  251. {reader-3.25 → reader-3.26}/tests/test_reader_lifecycle.py +0 -0
  252. {reader-3.25 → reader-3.26}/tests/test_reader_private.py +0 -0
  253. {reader-3.25 → reader-3.26}/tests/test_reader_search.py +0 -0
  254. {reader-3.25 → reader-3.26}/tests/test_reader_sort.py +0 -0
  255. {reader-3.25 → reader-3.26}/tests/test_reader_update.py +0 -0
  256. {reader-3.25 → reader-3.26}/tests/test_reader_utils.py +0 -0
  257. {reader-3.25 → reader-3.26}/tests/test_search.py +0 -0
  258. {reader-3.25 → reader-3.26}/tests/test_sql_utils.py +0 -0
  259. {reader-3.25 → reader-3.26}/tests/test_sqlite_utils.py +0 -0
  260. {reader-3.25 → reader-3.26}/tests/test_tags.py +0 -0
  261. {reader-3.25 → reader-3.26}/tests/test_types.py +0 -0
  262. {reader-3.25 → reader-3.26}/tests/utils.py +0 -0
  263. {reader-3.25 → reader-3.26}/tox.ini +0 -0
@@ -6,6 +6,18 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.26
10
+ ------------
11
+
12
+ Released 2026-06-24
13
+
14
+ * Allow exporting the entire database (e.g. for backup or migration).
15
+
16
+ * Through the :meth:`~reader._types.StorageType.export` storage method.
17
+ * In the web app, on the settings page; :ref:`screenshots <screenshots-export>`.
18
+ * In the CLI, as a new ``export`` command.
19
+
20
+
9
21
  Version 3.25
10
22
  ------------
11
23
 
@@ -15,7 +27,8 @@ Released 2026-05-25
15
27
 
16
28
  * Add :mod:`~reader.plugins.autodiscover` built-in plugin.
17
29
  * Add :mod:`reader.discover` module providing low level discovery.
18
- * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions when adding a feed.
30
+ * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
31
+ when adding a feed; :ref:`screenshots <screenshots-autodiscovery>`.
19
32
 
20
33
  * Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
21
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.25
3
+ Version: 3.26
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -49,6 +49,8 @@ Provides-Extra: app
49
49
  Requires-Dist: flask>=0.10; extra == "app"
50
50
  Requires-Dist: humanize>=4; extra == "app"
51
51
  Requires-Dist: PyYAML; extra == "app"
52
+ Requires-Dist: click; extra == "app"
53
+ Requires-Dist: platformdirs; extra == "app"
52
54
  Requires-Dist: WTForms; extra == "app"
53
55
  Requires-Dist: Flask-WTF; extra == "app"
54
56
  Requires-Dist: jinja2-fragments; extra == "app"
@@ -19,6 +19,8 @@ Features, as of *reader* version |version|:
19
19
  * article view
20
20
  * custom feed titles
21
21
  * dark mode
22
+ * feed autodiscovery
23
+ * full data export
22
24
 
23
25
  More features already implemented in the library
24
26
  and the :ref:`legacy web app <legacy web app>`
@@ -86,6 +88,20 @@ Screenshots
86
88
 
87
89
  contextual help
88
90
 
91
+ .. _screenshots-autodiscovery:
92
+
93
+ .. figure:: screenshots/autodiscovery.png
94
+ :width: 240px
95
+
96
+ feed autodiscovery
97
+
98
+ .. _screenshots-export:
99
+
100
+ .. figure:: screenshots/export-database.png
101
+ :width: 240px
102
+
103
+ database exports
104
+
89
105
 
90
106
  Serving the web app
91
107
  -------------------
@@ -139,15 +139,18 @@ since each connection would be to a *different* database::
139
139
 
140
140
  .. _backups:
141
141
 
142
- Back-ups
143
- ~~~~~~~~
142
+ Backups
143
+ ~~~~~~~
144
144
 
145
- Making back-ups depends on the storage used.
145
+ Making backups generally depends on the storage used.
146
146
 
147
- For the SQLite storage, you should use
148
- the `sqlite3 .backup command <https://sqlite.org/cli.html>`_
149
- or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_
150
- (see :gh:`backup.sh <scripts/backup.sh>` for an example).
147
+ If the storage implements it, you can use
148
+ the :meth:`~reader._types.StorageType.export` method
149
+ or the ``export`` :doc:`CLI <cli>` command.
150
+
151
+ For the SQLite storage, you can also use
152
+ the `.backup command <https://sqlite.org/cli.html>`_
153
+ or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_.
151
154
 
152
155
 
153
156
 
@@ -65,8 +65,11 @@ cli = [
65
65
  app = [
66
66
  "flask>=0.10",
67
67
  "humanize>=4",
68
- # for config
68
+ # for legacy (tags)
69
69
  "PyYAML",
70
+ # for cli
71
+ "click",
72
+ "platformdirs",
70
73
  # for v2
71
74
  "WTForms",
72
75
  "Flask-WTF",
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.25'
40
+ __version__ = '3.26'
41
41
 
42
42
  # isort: off
43
43
 
@@ -19,6 +19,7 @@ from flask import render_template
19
19
  from flask import render_template_string
20
20
  from flask import request
21
21
  from flask import Response
22
+ from flask import send_from_directory
22
23
  from flask import stream_with_context
23
24
  from flask import url_for
24
25
  from flask_wtf.csrf import CSRFError
@@ -35,6 +36,9 @@ from reader import InvalidFeedURLError
35
36
  from reader import opml
36
37
  from reader import UpdateError
37
38
 
39
+ from .exports import ExportNotFoundError
40
+ from .exports import TooManyExportsError
41
+ from .ext import get_exports
38
42
  from .ext import get_reader
39
43
  from .ext import ReaderExtension
40
44
  from .forms import AddFeed
@@ -94,7 +98,7 @@ def entries():
94
98
  hostname = urlparse(feed.url).hostname
95
99
  autodiscover = [
96
100
  (link, hostname and urlparse(link['href']).hostname == hostname)
97
- for link in reader.get_tag(feed, autodiscover_tag, None)
101
+ for link in reader.get_tag(feed, autodiscover_tag, ())
98
102
  ]
99
103
 
100
104
  kwargs = dict(form.data)
@@ -356,6 +360,38 @@ def help():
356
360
  return render_template('help.html')
357
361
 
358
362
 
363
+ @blueprint.route('/settings')
364
+ def settings():
365
+ return render_template('settings.html', exports=get_exports().list())
366
+
367
+
368
+ @blueprint.route('/settings/exports', methods=['POST'])
369
+ def create_export():
370
+ try:
371
+ get_exports().create()
372
+ except TooManyExportsError:
373
+ flash("Too many exports, delete some of them first.", 'exports.danger')
374
+ return redirect(url_for('.settings', _anchor='exports'), code=303)
375
+
376
+
377
+ @blueprint.route('/settings/exports/<name>')
378
+ def get_export(name):
379
+ exports = get_exports()
380
+ if not exports.exists(name):
381
+ abort(404)
382
+ return send_from_directory(exports.path, name, as_attachment=True)
383
+
384
+
385
+ @blueprint.route('/settings/exports/<name>/delete', methods=['POST'])
386
+ def delete_export(name):
387
+ # flask should ensure <name> doesn't contain slashes etc.
388
+ try:
389
+ get_exports().delete(name)
390
+ except ExportNotFoundError:
391
+ abort(404)
392
+ return redirect(url_for('.settings', _anchor='exports'), code=303)
393
+
394
+
359
395
  def stream_template(template_name_or_list, **kwargs):
360
396
  # Ensure flashed messages get removed from the session,
361
397
  # otherwise they keep adding up and never disappear.
@@ -1,5 +1,9 @@
1
+ import pathlib
2
+
1
3
  import click
4
+ import platformdirs
2
5
 
6
+ from reader._cli import app_name
3
7
  from reader._cli import load_reader_config_from_context
4
8
  from reader._cli import setup_logging
5
9
 
@@ -26,9 +30,16 @@ def make_add_response_headers_middleware(wsgi_app, headers):
26
30
  help="Import path to a web app plug-in. Can be passed multiple times.",
27
31
  )
28
32
  @click.option('--legacy/--no-legacy', help="Serve the legacy app.")
33
+ @click.option(
34
+ '--cache-dir',
35
+ type=click.Path(file_okay=False, path_type=pathlib.Path),
36
+ default=platformdirs.user_cache_dir(app_name),
37
+ show_default=True,
38
+ help="The directory to store cache data in.",
39
+ )
29
40
  @click.option('-v', '--verbose', count=True)
30
41
  @click.pass_context
31
- def cli(ctx, host, port, plugins, legacy, verbose):
42
+ def cli(ctx, host, port, plugins, legacy, cache_dir, verbose):
32
43
  """Run a local HTTP reader server."""
33
44
 
34
45
  setup_logging(verbose)
@@ -0,0 +1,58 @@
1
+ import pathlib
2
+ from datetime import datetime
3
+ from datetime import timezone
4
+
5
+
6
+ class TooManyExportsError(Exception):
7
+ pass
8
+
9
+
10
+ class ExportNotFoundError(Exception):
11
+ pass
12
+
13
+
14
+ class Exports:
15
+
16
+ max_files = 2
17
+ prefix_fmt = "reader.%Y-%m-%d-%H-%M-%S"
18
+
19
+ def __init__(self, reader, path):
20
+ self.reader = reader
21
+ self.path = pathlib.Path(path)
22
+ self.path.mkdir(parents=True, exist_ok=True)
23
+
24
+ def parse_name(self, name):
25
+ name_fmt = self.reader._storage.export_name(self.prefix_fmt)
26
+ try:
27
+ dt = datetime.strptime(name, name_fmt)
28
+ except ValueError:
29
+ return None
30
+ else:
31
+ return dt.replace(tzinfo=timezone.utc)
32
+
33
+ def create(self):
34
+ # not looking at file names accounts for exports in progress
35
+ if len(list(self.path.iterdir())) >= self.max_files:
36
+ raise TooManyExportsError
37
+ prefix = self.reader._now().strftime(self.prefix_fmt)
38
+ self.reader._storage.export(self.path, prefix)
39
+
40
+ def exists(self, name):
41
+ if not self.parse_name(name):
42
+ return False
43
+ return (self.path / name).exists()
44
+
45
+ def list(self):
46
+ rv = []
47
+ for path in self.path.iterdir():
48
+ if dt := self.parse_name(path.name):
49
+ rv.append((path, dt))
50
+ return rv
51
+
52
+ def delete(self, name):
53
+ if not self.parse_name(name):
54
+ raise ExportNotFoundError
55
+ try:
56
+ (self.path / name).unlink()
57
+ except FileNotFoundError:
58
+ raise ExportNotFoundError from None
@@ -3,6 +3,7 @@ Flask extension that encapsulates configurable reader-related aspects.
3
3
 
4
4
  """
5
5
 
6
+ import pathlib
6
7
  from dataclasses import dataclass
7
8
 
8
9
  from flask import current_app
@@ -10,13 +11,20 @@ from flask import current_app
10
11
  from reader import make_reader
11
12
  from reader.plugins._loader import PluginLoader
12
13
 
13
- _plugin_loader = PluginLoader('init_app')
14
+ from .exports import Exports
14
15
 
15
16
 
16
17
  def get_reader():
17
18
  return current_app.extensions['reader'].get_reader()
18
19
 
19
20
 
21
+ def get_exports():
22
+ return current_app.extensions['reader'].get_exports()
23
+
24
+
25
+ _plugin_loader = PluginLoader('init_app')
26
+
27
+
20
28
  class ReaderExtension:
21
29
 
22
30
  def __init__(self, app=None, state_cls=None):
@@ -26,26 +34,45 @@ class ReaderExtension:
26
34
 
27
35
  def init_app(self, app):
28
36
  config = app.config['READER']
29
- app.extensions['reader'] = self.state_cls(config[''])
37
+ app.extensions['reader'] = self.state_cls(
38
+ config[''], config.get('web', {}).get('cache_dir')
39
+ )
30
40
  _plugin_loader.oneshot(app, config.get('web', {}).get('plugins', ()))
31
41
 
32
42
 
33
43
  @dataclass
34
44
  class ReaderStateBase:
35
45
  args: dict
46
+ cache_dir: pathlib.Path | None
36
47
 
37
48
  def get_reader(self):
38
49
  raise NotImplementedError
39
50
 
51
+ def get_user_cache_dir(self):
52
+ raise NotImplementedError
53
+
40
54
  def _make_reader(self, **kwargs):
41
55
  return make_reader(**self.args | kwargs)
42
56
 
57
+ def get_exports(self):
58
+ cache_dir = self.get_user_cache_dir()
59
+ if not cache_dir:
60
+ return None
61
+ return Exports(self.get_reader(), cache_dir / 'exports')
62
+
43
63
 
44
64
  class _ReaderState(ReaderStateBase):
45
65
 
46
66
  def __init__(self, *args, **kwargs):
47
67
  super().__init__(*args, **kwargs)
48
68
  self._reader = self._make_reader()
69
+ self._exports = super().get_exports()
49
70
 
50
71
  def get_reader(self):
51
72
  return self._reader
73
+
74
+ def get_user_cache_dir(self):
75
+ return self.cache_dir
76
+
77
+ def get_exports(self):
78
+ return self._exports
@@ -69,8 +69,6 @@
69
69
  height: auto;
70
70
  }
71
71
 
72
-
73
- :target {
74
- outline: 0;
75
- box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color);
72
+ h1:target, h2:target, h3:target, h4:target, h5:target, h6:target {
73
+ text-decoration: underline;
76
74
  }
@@ -19,7 +19,7 @@
19
19
  {{ macros.flashed_messages() }}
20
20
 
21
21
  {%- if feed and not feed.url.startswith('reader:') %}
22
- <ul class="list-unstyled mt-3">
22
+ <ul class="list-unstyled">
23
23
 
24
24
  {%- if not feed.last_updated and not feed.last_exception %}
25
25
  <li class="alert alert-secondary">
@@ -62,10 +62,13 @@
62
62
  <ul class="navbar-nav mb-0">
63
63
  {% block right_hidden_navbar %}
64
64
 
65
+ {% block right_hidden_navbar_common %}
66
+
65
67
  <li class="nav-item">
66
68
  <a
67
69
  href="{{ url_for('reader.help', _anchor=help_topic | default(none)) }}"
68
70
  class="btn btn-link nav-link d-flex align-items-center"
71
+ title="help"
69
72
  >
70
73
  <i class="bi bi-question-lg"></i>
71
74
  <span class="d-md-none ms-2">help</span>
@@ -78,7 +81,7 @@
78
81
  type="button" id="theme"
79
82
  data-bs-toggle="dropdown" data-bs-display="static"
80
83
  aria-expanded="false" aria-label="toggle theme (auto)">
81
- <i class="bi bi-circle-half theme-icon-active"></i>
84
+ <i class="bi bi-circle-half theme-icon-active" title="toggle theme"></i>
82
85
  <span class="d-md-none ms-2" id="theme-text">toggle theme</span>
83
86
  </button>
84
87
  <ul class="dropdown-menu dropdown-menu-end mb-2" aria-labelledby="theme-text">
@@ -110,6 +113,19 @@
110
113
  </li>
111
114
 
112
115
  {% endblock %}
116
+
117
+ <li class="nav-item">
118
+ <a
119
+ href="{{ url_for('reader.settings') }}"
120
+ class="btn btn-link nav-link d-flex align-items-center"
121
+ title="settings"
122
+ >
123
+ <i class="bi bi-gear-fill"></i>
124
+ <span class="d-md-none ms-2">settings</span>
125
+ </a>
126
+ </li>
127
+
128
+ {% endblock %}
113
129
  </ul>
114
130
  </div>
115
131
  </div>
@@ -1,9 +1,10 @@
1
1
 
2
- {% macro flashed_messages() %}
3
- {%- if get_flashed_messages() %}
2
+ {% macro flashed_messages(category_filter=()) %}
3
+ {%- set messages = get_flashed_messages(True, category_filter) %}
4
+ {%- if messages %}
4
5
  <ul class="list-unstyled">
5
- {%- for category, message in get_flashed_messages(with_categories=true) %}
6
- <li class="alert alert-{{ category }}">{{ message }}</li>
6
+ {%- for category, message in messages %}
7
+ <li class="alert alert-{{ category.rpartition('.')[2] }}">{{ message }}</li>
7
8
  {%- endfor %}
8
9
  </ul>
9
10
  {%- endif %}
@@ -0,0 +1 @@
1
+ {% extends "settings_base.html" %}
@@ -0,0 +1,67 @@
1
+ {% extends "layout.html" %}
2
+
3
+ {% import "macros.html" as macros %}
4
+
5
+
6
+ {% block page_title %}
7
+ Settings - {{ super() }}
8
+ {% endblock %}
9
+
10
+
11
+ {% block body %}
12
+
13
+ <h1>Settings</h1>
14
+
15
+
16
+ <h2 id="exports">Export database</h2>
17
+
18
+ <p>
19
+ Export your entire <em>reader</em> database,
20
+ for backup or use with another installation.
21
+
22
+ {{ macros.flashed_messages(['exports.danger']) }}
23
+
24
+ <div class="card my-3">
25
+ <div class="card-header">
26
+ Recent exports
27
+ </div>
28
+
29
+ <ul class="list-group list-group-flush">
30
+ {%- for path, date in exports | sort(reverse=True) %}
31
+ <li class="list-group-item d-flex flex-wrap justify-content-between align-items-center gap-2">
32
+ <span>
33
+ <a href="{{ url_for('.get_export', name=path.name) }}" class="d-inline-block">
34
+ <time datetime="{{ date }}" title="{{ date }}">
35
+ {{ date | humanize_naturaltime }}
36
+ </time>
37
+ </a>
38
+ <span class="text-secondary">({{ path.stat().st_size | filesizeformat }})</span>
39
+ </span>
40
+
41
+ <form action="{{ url_for('.delete_export', name=path.name) }}" method="post">
42
+ {{ macros.csrf_token_field() }}
43
+ <input type="submit" class="btn btn-outline-danger btn-sm" value="delete">
44
+ </form>
45
+
46
+ </li>
47
+ {%- else %}
48
+ <li class="list-group-item">
49
+ no exports found
50
+ </li>
51
+ {%- endfor %}
52
+ </ul>
53
+
54
+ </div>
55
+
56
+ <form action="{{ url_for('.create_export') }}" method="post" class="my-3">
57
+ {{ macros.csrf_token_field() }}
58
+ <input type="submit" class="btn btn-secondary" value="create new export">
59
+ </form>
60
+
61
+ <p class="text-secondary">
62
+ To export feeds as a subscription list, click the
63
+ <i class="bi bi-filetype-xml" aria-label="export as OPML"></i> icon
64
+ on the <a href="{{ url_for('.feeds') }}">Feeds</a> page.
65
+
66
+
67
+ {% endblock %}
@@ -459,6 +459,18 @@ def search_entries(reader, query):
459
459
  click.echo(f"{entry.feed.url} {entry.link or entry.id}")
460
460
 
461
461
 
462
+ @cli.command()
463
+ @click.argument('outdir', type=click.Path(exists=True, file_okay=False), default='.')
464
+ @click.option('--prefix', default="reader.%Y-%m-%d-%H-%M-%S", show_default=True)
465
+ @pass_reader
466
+ def export(reader, outdir, prefix):
467
+ """Export all data to a file in OUTDIR (defaults to cwd)."""
468
+
469
+ concrete_prefix = reader._now().strftime(prefix)
470
+ export_path = reader._storage.export(outdir, concrete_prefix)
471
+ click.echo(export_path)
472
+
473
+
462
474
  try:
463
475
  import reader._app.cli
464
476
  except ImportError:
@@ -2,12 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import pathlib
5
6
  import sqlite3
6
7
  import sys
7
8
  from collections.abc import Callable
8
9
  from collections.abc import Iterable
9
10
  from functools import partial
10
11
  from typing import Any
12
+ from typing import Self
11
13
  from typing import TypeVar
12
14
 
13
15
  from ..exceptions import StorageError
@@ -82,8 +84,9 @@ class StorageBase:
82
84
  )
83
85
 
84
86
  @wrap_exceptions()
85
- def __enter__(self) -> None:
87
+ def __enter__(self) -> Self:
86
88
  self.factory.__enter__()
89
+ return self
87
90
 
88
91
  @wrap_exceptions()
89
92
  def __exit__(self, *_: Any) -> None:
@@ -109,3 +112,30 @@ class StorageBase:
109
112
  last,
110
113
  row_factory,
111
114
  )
115
+
116
+ def export(self, outdir: str | os.PathLike[str], prefix: str) -> pathlib.Path:
117
+ import gzip
118
+ import shutil
119
+ import tempfile
120
+
121
+ outdir = pathlib.Path(outdir)
122
+ gz = outdir / self.export_name(prefix)
123
+
124
+ with tempfile.TemporaryDirectory(f".{prefix}", dir=outdir) as tmpdir_str:
125
+ tmpdir = pathlib.Path(tmpdir_str)
126
+
127
+ tmp_db = tmpdir / 'db'
128
+ self.get_db().execute("vacuum into ?", (str(tmp_db),))
129
+
130
+ # the file name is stored in the .gz file
131
+ tmp_gz = tmpdir / gz.with_suffix('').name
132
+ with tmp_db.open('rb') as db_file:
133
+ with gzip.open(tmp_gz, 'wb') as gz_file:
134
+ shutil.copyfileobj(db_file, gz_file)
135
+
136
+ tmp_gz.replace(gz)
137
+
138
+ return gz
139
+
140
+ def export_name(self, prefix: str) -> str:
141
+ return f"{prefix}.sqlite.gz"
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
4
+ import pathlib
3
5
  from collections.abc import Iterable
4
6
  from collections.abc import Mapping
5
7
  from collections.abc import Sequence
@@ -727,7 +729,7 @@ class StorageType(Protocol): # pragma: no cover
727
729
 
728
730
  """
729
731
 
730
- def __enter__(self) -> None:
732
+ def __enter__(self) -> Self:
731
733
  """Called when :class:`.Reader` is used as a context manager."""
732
734
 
733
735
  def __exit__(self, *_: Any) -> None:
@@ -1113,6 +1115,17 @@ class StorageType(Protocol): # pragma: no cover
1113
1115
 
1114
1116
  """
1115
1117
 
1118
+ def export(self, outdir: str | os.PathLike[str], prefix: str) -> pathlib.Path:
1119
+ """Export all data to a file in ``outdir`` and return its full path.
1120
+
1121
+ Aside from starting with ``prefix``,
1122
+ the file name and contents are storage-dependent.
1123
+
1124
+ """
1125
+
1126
+ def export_name(self, prefix: str) -> str:
1127
+ """Return the file name of an export."""
1128
+
1116
1129
 
1117
1130
  @runtime_checkable
1118
1131
  class BoundSearchStorageType(StorageType, Protocol):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.25
3
+ Version: 3.26
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -49,6 +49,8 @@ Provides-Extra: app
49
49
  Requires-Dist: flask>=0.10; extra == "app"
50
50
  Requires-Dist: humanize>=4; extra == "app"
51
51
  Requires-Dist: PyYAML; extra == "app"
52
+ Requires-Dist: click; extra == "app"
53
+ Requires-Dist: platformdirs; extra == "app"
52
54
  Requires-Dist: WTForms; extra == "app"
53
55
  Requires-Dist: Flask-WTF; extra == "app"
54
56
  Requires-Dist: jinja2-fragments; extra == "app"
@@ -20,11 +20,13 @@ docs/internal.rst
20
20
  docs/plugins.rst
21
21
  docs/tutorial.rst
22
22
  docs/why.rst
23
+ docs/screenshots/autodiscovery.png
23
24
  docs/screenshots/empty.png
24
25
  docs/screenshots/entries-filters-light.png
25
26
  docs/screenshots/entries.png
26
27
  docs/screenshots/entry-image-light.png
27
28
  docs/screenshots/entry.png
29
+ docs/screenshots/export-database.png
28
30
  docs/screenshots/feed.png
29
31
  docs/screenshots/feeds.png
30
32
  docs/screenshots/help.png
@@ -36,7 +38,6 @@ examples/feed_slugs.py
36
38
  examples/parser_only.py
37
39
  examples/podcast.py
38
40
  examples/terminal.py
39
- scripts/backup.sh
40
41
  scripts/bench.py
41
42
  scripts/debug_storage_stats.py
42
43
  scripts/entry_dedupe_backtest.py
@@ -67,6 +68,7 @@ src/reader.egg-info/requires.txt
67
68
  src/reader.egg-info/top_level.txt
68
69
  src/reader/_app/__init__.py
69
70
  src/reader/_app/cli.py
71
+ src/reader/_app/exports.py
70
72
  src/reader/_app/ext.py
71
73
  src/reader/_app/forms.py
72
74
  src/reader/_app/wsgi.py
@@ -104,6 +106,8 @@ src/reader/_app/templates/import_feeds.html
104
106
  src/reader/_app/templates/layout.html
105
107
  src/reader/_app/templates/layout_base.html
106
108
  src/reader/_app/templates/macros.html
109
+ src/reader/_app/templates/settings.html
110
+ src/reader/_app/templates/settings_base.html
107
111
  src/reader/_parser/__init__.py
108
112
  src/reader/_parser/_http_utils.py
109
113
  src/reader/_parser/_url_utils.py