reader 3.25__tar.gz → 3.27__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.27}/CHANGES.rst +38 -2
  2. {reader-3.25/src/reader.egg-info → reader-3.27}/PKG-INFO +5 -3
  3. {reader-3.25 → reader-3.27}/docs/api.rst +9 -10
  4. {reader-3.25 → reader-3.27}/docs/app.rst +16 -0
  5. {reader-3.25 → reader-3.27}/docs/conf.py +15 -1
  6. {reader-3.25 → reader-3.27}/docs/guide.rst +20 -7
  7. {reader-3.25 → reader-3.27}/docs/install.rst +6 -8
  8. {reader-3.25 → reader-3.27}/docs/internal.rst +1 -1
  9. reader-3.27/docs/screenshots/autodiscovery.png +0 -0
  10. reader-3.27/docs/screenshots/export-database.png +0 -0
  11. {reader-3.25 → reader-3.27}/pyproject.toml +9 -4
  12. {reader-3.25 → reader-3.27}/scripts/release.py +1 -11
  13. {reader-3.25 → reader-3.27}/src/reader/__init__.py +1 -1
  14. {reader-3.25 → reader-3.27}/src/reader/_app/__init__.py +37 -1
  15. {reader-3.25 → reader-3.27}/src/reader/_app/cli.py +12 -1
  16. reader-3.27/src/reader/_app/exports.py +58 -0
  17. {reader-3.25 → reader-3.27}/src/reader/_app/ext.py +29 -2
  18. {reader-3.25 → reader-3.27}/src/reader/_app/static/style.css +2 -4
  19. {reader-3.25 → reader-3.27}/src/reader/_app/templates/entries.html +1 -1
  20. {reader-3.25 → reader-3.27}/src/reader/_app/templates/layout_base.html +17 -1
  21. {reader-3.25 → reader-3.27}/src/reader/_app/templates/macros.html +5 -4
  22. reader-3.27/src/reader/_app/templates/settings.html +1 -0
  23. reader-3.27/src/reader/_app/templates/settings_base.html +67 -0
  24. {reader-3.25 → reader-3.27}/src/reader/_cli.py +20 -1
  25. {reader-3.25 → reader-3.27}/src/reader/_parser/__init__.py +57 -140
  26. {reader-3.25 → reader-3.27}/src/reader/_parser/_url_utils.py +18 -0
  27. {reader-3.25 → reader-3.27}/src/reader/_parser/feedparser.py +3 -8
  28. {reader-3.25 → reader-3.27}/src/reader/_parser/file.py +2 -2
  29. {reader-3.25 → reader-3.27}/src/reader/_parser/http.py +69 -17
  30. {reader-3.25 → reader-3.27}/src/reader/_parser/jsonfeed.py +5 -14
  31. {reader-3.25 → reader-3.27}/src/reader/_storage/__init__.py +6 -2
  32. {reader-3.25 → reader-3.27}/src/reader/_storage/_base.py +52 -11
  33. {reader-3.25 → reader-3.27}/src/reader/_storage/_changes.py +1 -1
  34. {reader-3.25 → reader-3.27}/src/reader/_storage/_entries.py +8 -4
  35. {reader-3.25 → reader-3.27}/src/reader/_storage/_feeds.py +1 -1
  36. {reader-3.25 → reader-3.27}/src/reader/_storage/_html_utils.py +1 -1
  37. {reader-3.25 → reader-3.27}/src/reader/_storage/_search.py +1 -5
  38. {reader-3.25 → reader-3.27}/src/reader/_storage/_sql_utils.py +10 -16
  39. {reader-3.25 → reader-3.27}/src/reader/_storage/_sqlite_utils.py +3 -8
  40. {reader-3.25 → reader-3.27}/src/reader/_storage/_tags.py +7 -11
  41. {reader-3.25 → reader-3.27}/src/reader/_types.py +31 -27
  42. {reader-3.25 → reader-3.27}/src/reader/_update/__init__.py +4 -3
  43. {reader-3.25 → reader-3.27}/src/reader/_update/base.py +14 -20
  44. {reader-3.25 → reader-3.27}/src/reader/_update/hooks.py +2 -5
  45. {reader-3.25 → reader-3.27}/src/reader/_utils.py +17 -37
  46. {reader-3.25 → reader-3.27}/src/reader/core.py +48 -83
  47. {reader-3.25 → reader-3.27}/src/reader/discover.py +4 -2
  48. {reader-3.25 → reader-3.27}/src/reader/exceptions.py +5 -14
  49. {reader-3.25 → reader-3.27}/src/reader/plugins/__init__.py +1 -8
  50. {reader-3.25 → reader-3.27}/src/reader/plugins/_loader.py +8 -12
  51. {reader-3.25 → reader-3.27}/src/reader/plugins/autodiscover.py +7 -16
  52. {reader-3.25 → reader-3.27}/src/reader/types.py +33 -59
  53. {reader-3.25 → reader-3.27/src/reader.egg-info}/PKG-INFO +5 -3
  54. {reader-3.25 → reader-3.27}/src/reader.egg-info/SOURCES.txt +5 -36
  55. {reader-3.25 → reader-3.27}/src/reader.egg-info/requires.txt +2 -0
  56. {reader-3.25 → reader-3.27}/tests/fakeparser.py +3 -3
  57. {reader-3.25 → reader-3.27}/tests/test_lazy_imports.py +4 -4
  58. {reader-3.25 → reader-3.27}/tests/test_parser.py +93 -44
  59. {reader-3.25 → reader-3.27}/tests/test_plugins_autodiscover.py +1 -1
  60. {reader-3.25 → reader-3.27}/tests/test_reader_hooks.py +3 -4
  61. {reader-3.25 → reader-3.27}/tests/test_reader_lifecycle.py +26 -0
  62. {reader-3.25 → reader-3.27}/tests/test_reader_private.py +1 -0
  63. {reader-3.25 → reader-3.27}/tests/test_reader_update.py +29 -76
  64. {reader-3.25 → reader-3.27}/tests/test_storage.py +16 -0
  65. {reader-3.25 → reader-3.27}/tests/test_types.py +16 -0
  66. {reader-3.25 → reader-3.27}/tox.ini +5 -4
  67. reader-3.25/scripts/backup.sh +0 -36
  68. reader-3.25/src/reader/_vendor/feedparser/__init__.py +0 -88
  69. reader-3.25/src/reader/_vendor/feedparser/api.py +0 -377
  70. reader-3.25/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -73
  71. reader-3.25/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -80
  72. reader-3.25/src/reader/_vendor/feedparser/datetimes/greek.py +0 -90
  73. reader-3.25/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -66
  74. reader-3.25/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -160
  75. reader-3.25/src/reader/_vendor/feedparser/datetimes/korean.py +0 -94
  76. reader-3.25/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -63
  77. reader-3.25/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -179
  78. reader-3.25/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -128
  79. reader-3.25/src/reader/_vendor/feedparser/encodings.py +0 -649
  80. reader-3.25/src/reader/_vendor/feedparser/exceptions.py +0 -55
  81. reader-3.25/src/reader/_vendor/feedparser/html.py +0 -352
  82. reader-3.25/src/reader/_vendor/feedparser/http.py +0 -78
  83. reader-3.25/src/reader/_vendor/feedparser/mixin.py +0 -842
  84. reader-3.25/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  85. reader-3.25/src/reader/_vendor/feedparser/namespaces/_base.py +0 -547
  86. reader-3.25/src/reader/_vendor/feedparser/namespaces/admin.py +0 -53
  87. reader-3.25/src/reader/_vendor/feedparser/namespaces/cc.py +0 -70
  88. reader-3.25/src/reader/_vendor/feedparser/namespaces/dc.py +0 -138
  89. reader-3.25/src/reader/_vendor/feedparser/namespaces/georss.py +0 -683
  90. reader-3.25/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -113
  91. reader-3.25/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -142
  92. reader-3.25/src/reader/_vendor/feedparser/namespaces/psc.py +0 -74
  93. reader-3.25/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  94. reader-3.25/src/reader/_vendor/feedparser/parsers/json.py +0 -135
  95. reader-3.25/src/reader/_vendor/feedparser/parsers/loose.py +0 -75
  96. reader-3.25/src/reader/_vendor/feedparser/parsers/strict.py +0 -141
  97. reader-3.25/src/reader/_vendor/feedparser/sanitizer.py +0 -974
  98. reader-3.25/src/reader/_vendor/feedparser/sgml.py +0 -98
  99. reader-3.25/src/reader/_vendor/feedparser/urls.py +0 -174
  100. reader-3.25/src/reader/_vendor/feedparser/util.py +0 -158
  101. reader-3.25/src/reader/py.typed +0 -0
  102. reader-3.25/tests/_plugins/__init__.py +0 -0
  103. {reader-3.25 → reader-3.27}/LICENSE +0 -0
  104. {reader-3.25 → reader-3.27}/MANIFEST.in +0 -0
  105. {reader-3.25 → reader-3.27}/README.rst +0 -0
  106. {reader-3.25 → reader-3.27}/docs/changelog.rst +0 -0
  107. {reader-3.25 → reader-3.27}/docs/cli.rst +0 -0
  108. {reader-3.25 → reader-3.27}/docs/config.rst +0 -0
  109. {reader-3.25 → reader-3.27}/docs/contributing.rst +0 -0
  110. {reader-3.25 → reader-3.27}/docs/dev.rst +0 -0
  111. {reader-3.25 → reader-3.27}/docs/index.rst +0 -0
  112. {reader-3.25 → reader-3.27}/docs/plugins.rst +0 -0
  113. {reader-3.25 → reader-3.27}/docs/screenshots/empty.png +0 -0
  114. {reader-3.25 → reader-3.27}/docs/screenshots/entries-filters-light.png +0 -0
  115. {reader-3.25 → reader-3.27}/docs/screenshots/entries.png +0 -0
  116. {reader-3.25 → reader-3.27}/docs/screenshots/entry-image-light.png +0 -0
  117. {reader-3.25 → reader-3.27}/docs/screenshots/entry.png +0 -0
  118. {reader-3.25 → reader-3.27}/docs/screenshots/feed.png +0 -0
  119. {reader-3.25 → reader-3.27}/docs/screenshots/feeds.png +0 -0
  120. {reader-3.25 → reader-3.27}/docs/screenshots/help.png +0 -0
  121. {reader-3.25 → reader-3.27}/docs/screenshots/import-result.png +0 -0
  122. {reader-3.25 → reader-3.27}/docs/screenshots/import-select.png +0 -0
  123. {reader-3.25 → reader-3.27}/docs/tutorial.rst +0 -0
  124. {reader-3.25 → reader-3.27}/docs/why.rst +0 -0
  125. {reader-3.25 → reader-3.27}/examples/config.toml +0 -0
  126. {reader-3.25 → reader-3.27}/examples/custom_headers.py +0 -0
  127. {reader-3.25 → reader-3.27}/examples/feed_slugs.py +0 -0
  128. {reader-3.25 → reader-3.27}/examples/parser_only.py +0 -0
  129. {reader-3.25 → reader-3.27}/examples/podcast.py +0 -0
  130. {reader-3.25 → reader-3.27}/examples/terminal.py +0 -0
  131. {reader-3.25 → reader-3.27}/run.sh +0 -0
  132. {reader-3.25 → reader-3.27}/scripts/bench.py +0 -0
  133. {reader-3.25 → reader-3.27}/scripts/debug_storage_stats.py +0 -0
  134. {reader-3.25 → reader-3.27}/scripts/entry_dedupe_backtest.py +0 -0
  135. {reader-3.25 → reader-3.27}/scripts/jscontrols.html +0 -0
  136. {reader-3.25 → reader-3.27}/scripts/jscontrols.py +0 -0
  137. {reader-3.25 → reader-3.27}/scripts/lines.sh +0 -0
  138. {reader-3.25 → reader-3.27}/scripts/vendor_app_stuff.py +0 -0
  139. {reader-3.25 → reader-3.27}/setup.cfg +0 -0
  140. {reader-3.25 → reader-3.27}/src/reader/__main__.py +0 -0
  141. {reader-3.25 → reader-3.27}/src/reader/_app/forms.py +0 -0
  142. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/__init__.py +0 -0
  143. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/api_thing.py +0 -0
  144. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/static/controls.js +0 -0
  145. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/static/style.css +0 -0
  146. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/add_entry.html +0 -0
  147. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/entries.html +0 -0
  148. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/entry.html +0 -0
  149. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/feeds.html +0 -0
  150. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/layout.html +0 -0
  151. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/macros.html +0 -0
  152. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/metadata.html +0 -0
  153. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/templates/tags.html +0 -0
  154. {reader-3.25 → reader-3.27}/src/reader/_app/legacy/wsgi.py +0 -0
  155. {reader-3.25 → reader-3.27}/src/reader/_app/static/theme.js +0 -0
  156. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/README +0 -0
  157. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
  158. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  159. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  160. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
  161. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
  162. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
  163. {reader-3.25 → reader-3.27}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
  164. {reader-3.25 → reader-3.27}/src/reader/_app/templates/add_feed.html +0 -0
  165. {reader-3.25 → reader-3.27}/src/reader/_app/templates/change_feed_title.html +0 -0
  166. {reader-3.25 → reader-3.27}/src/reader/_app/templates/delete_feed.html +0 -0
  167. {reader-3.25 → reader-3.27}/src/reader/_app/templates/entry.html +0 -0
  168. {reader-3.25 → reader-3.27}/src/reader/_app/templates/feeds.html +0 -0
  169. {reader-3.25 → reader-3.27}/src/reader/_app/templates/help.html +0 -0
  170. {reader-3.25 → reader-3.27}/src/reader/_app/templates/import_feeds.html +0 -0
  171. {reader-3.25 → reader-3.27}/src/reader/_app/templates/layout.html +0 -0
  172. {reader-3.25 → reader-3.27}/src/reader/_app/wsgi.py +0 -0
  173. {reader-3.25 → reader-3.27}/src/reader/_config_utils.py +0 -0
  174. {reader-3.25 → reader-3.27}/src/reader/_hash_utils.py +0 -0
  175. {reader-3.25 → reader-3.27}/src/reader/_logging.py +0 -0
  176. {reader-3.25 → reader-3.27}/src/reader/_parser/_http_utils.py +0 -0
  177. {reader-3.25 → reader-3.27}/src/reader/_plugins/__init__.py +0 -0
  178. {reader-3.25 → reader-3.27}/src/reader/_plugins/cli_status.py +0 -0
  179. {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/__init__.py +0 -0
  180. {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/enclosure_tags.py +0 -0
  181. {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/preview_feed_list.py +0 -0
  182. {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/share.py +0 -0
  183. {reader-3.25 → reader-3.27}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
  184. {reader-3.25 → reader-3.27}/src/reader/_plugins/sqlite_releases.py +0 -0
  185. {reader-3.25 → reader-3.27}/src/reader/_plugins/timer.py +0 -0
  186. {reader-3.25 → reader-3.27}/src/reader/_storage/_schema.py +0 -0
  187. {reader-3.25 → reader-3.27}/src/reader/opml.py +0 -0
  188. {reader-3.25 → reader-3.27}/src/reader/plugins/enclosure_dedupe.py +0 -0
  189. {reader-3.25 → reader-3.27}/src/reader/plugins/entry_dedupe.py +0 -0
  190. {reader-3.25 → reader-3.27}/src/reader/plugins/mark_as_read.py +0 -0
  191. {reader-3.25 → reader-3.27}/src/reader/plugins/readtime.py +0 -0
  192. {reader-3.25 → reader-3.27}/src/reader/plugins/ua_fallback.py +0 -0
  193. {reader-3.25/src/reader/_vendor/feedparser → reader-3.27/src/reader}/py.typed +0 -0
  194. {reader-3.25 → reader-3.27}/src/reader/utils.py +0 -0
  195. {reader-3.25 → reader-3.27}/src/reader.egg-info/dependency_links.txt +0 -0
  196. {reader-3.25 → reader-3.27}/src/reader.egg-info/top_level.txt +0 -0
  197. {reader-3.25/src/reader/_vendor → reader-3.27/tests/_plugins}/__init__.py +0 -0
  198. {reader-3.25 → reader-3.27}/tests/_plugins/builtin.py +0 -0
  199. {reader-3.25 → reader-3.27}/tests/_plugins/error_during_import.py +0 -0
  200. {reader-3.25 → reader-3.27}/tests/_plugins/error_import_error.py +0 -0
  201. {reader-3.25 → reader-3.27}/tests/conftest.py +0 -0
  202. {reader-3.25 → reader-3.27}/tests/data/10.json +0 -0
  203. {reader-3.25 → reader-3.27}/tests/data/10.json.py +0 -0
  204. {reader-3.25 → reader-3.27}/tests/data/custom +0 -0
  205. {reader-3.25 → reader-3.27}/tests/data/empty.atom +0 -0
  206. {reader-3.25 → reader-3.27}/tests/data/empty.atom.py +0 -0
  207. {reader-3.25 → reader-3.27}/tests/data/empty.json +0 -0
  208. {reader-3.25 → reader-3.27}/tests/data/empty.json.py +0 -0
  209. {reader-3.25 → reader-3.27}/tests/data/empty.rss +0 -0
  210. {reader-3.25 → reader-3.27}/tests/data/empty.rss.py +0 -0
  211. {reader-3.25 → reader-3.27}/tests/data/full.atom +0 -0
  212. {reader-3.25 → reader-3.27}/tests/data/full.atom.py +0 -0
  213. {reader-3.25 → reader-3.27}/tests/data/full.json +0 -0
  214. {reader-3.25 → reader-3.27}/tests/data/full.json.py +0 -0
  215. {reader-3.25 → reader-3.27}/tests/data/full.rss +0 -0
  216. {reader-3.25 → reader-3.27}/tests/data/full.rss.py +0 -0
  217. {reader-3.25 → reader-3.27}/tests/data/invalid.json +0 -0
  218. {reader-3.25 → reader-3.27}/tests/data/invalid.json.py +0 -0
  219. {reader-3.25 → reader-3.27}/tests/data/relative.atom +0 -0
  220. {reader-3.25 → reader-3.27}/tests/data/relative.atom.py +0 -0
  221. {reader-3.25 → reader-3.27}/tests/data/relative.rss +0 -0
  222. {reader-3.25 → reader-3.27}/tests/data/relative.rss.py +0 -0
  223. {reader-3.25 → reader-3.27}/tests/data/sqlite_releases.html +0 -0
  224. {reader-3.25 → reader-3.27}/tests/data/unknown.json +0 -0
  225. {reader-3.25 → reader-3.27}/tests/data/unknown.json.py +0 -0
  226. {reader-3.25 → reader-3.27}/tests/fixtures.py +0 -0
  227. {reader-3.25 → reader-3.27}/tests/reader_methods.py +0 -0
  228. {reader-3.25 → reader-3.27}/tests/test__types.py +0 -0
  229. {reader-3.25 → reader-3.27}/tests/test__utils.py +0 -0
  230. {reader-3.25 → reader-3.27}/tests/test_app_legacy.py +0 -0
  231. {reader-3.25 → reader-3.27}/tests/test_app_wsgi.py +0 -0
  232. {reader-3.25 → reader-3.27}/tests/test_bench.py +0 -0
  233. {reader-3.25 → reader-3.27}/tests/test_changes.py +0 -0
  234. {reader-3.25 → reader-3.27}/tests/test_cli.py +0 -0
  235. {reader-3.25 → reader-3.27}/tests/test_config_utils.py +0 -0
  236. {reader-3.25 → reader-3.27}/tests/test_discover.py +0 -0
  237. {reader-3.25 → reader-3.27}/tests/test_exceptions.py +0 -0
  238. {reader-3.25 → reader-3.27}/tests/test_hash_utils.py +0 -0
  239. {reader-3.25 → reader-3.27}/tests/test_html_utils.py +0 -0
  240. {reader-3.25 → reader-3.27}/tests/test_logging.py +0 -0
  241. {reader-3.25 → reader-3.27}/tests/test_opml.py +0 -0
  242. {reader-3.25 → reader-3.27}/tests/test_plugins_cli_status.py +0 -0
  243. {reader-3.25 → reader-3.27}/tests/test_plugins_enclosure_dedupe.py +0 -0
  244. {reader-3.25 → reader-3.27}/tests/test_plugins_entry_dedupe.py +0 -0
  245. {reader-3.25 → reader-3.27}/tests/test_plugins_loader.py +0 -0
  246. {reader-3.25 → reader-3.27}/tests/test_plugins_mark_as_read.py +0 -0
  247. {reader-3.25 → reader-3.27}/tests/test_plugins_preview_feed_list.py +0 -0
  248. {reader-3.25 → reader-3.27}/tests/test_plugins_readtime.py +0 -0
  249. {reader-3.25 → reader-3.27}/tests/test_plugins_sqlite_releases.py +0 -0
  250. {reader-3.25 → reader-3.27}/tests/test_plugins_ua_fallback.py +0 -0
  251. {reader-3.25 → reader-3.27}/tests/test_reader.py +0 -0
  252. {reader-3.25 → reader-3.27}/tests/test_reader_counts.py +0 -0
  253. {reader-3.25 → reader-3.27}/tests/test_reader_deprecations.py +0 -0
  254. {reader-3.25 → reader-3.27}/tests/test_reader_filter.py +0 -0
  255. {reader-3.25 → reader-3.27}/tests/test_reader_integration.py +0 -0
  256. {reader-3.25 → reader-3.27}/tests/test_reader_search.py +0 -0
  257. {reader-3.25 → reader-3.27}/tests/test_reader_sort.py +0 -0
  258. {reader-3.25 → reader-3.27}/tests/test_reader_utils.py +0 -0
  259. {reader-3.25 → reader-3.27}/tests/test_search.py +0 -0
  260. {reader-3.25 → reader-3.27}/tests/test_sql_utils.py +0 -0
  261. {reader-3.25 → reader-3.27}/tests/test_sqlite_utils.py +0 -0
  262. {reader-3.25 → reader-3.27}/tests/test_tags.py +0 -0
  263. {reader-3.25 → reader-3.27}/tests/utils.py +0 -0
@@ -6,6 +6,41 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.27
10
+ ------------
11
+
12
+ Released 2026-10-11
13
+
14
+ * Add the ``migrate`` :func:`make_reader` argument.
15
+ Thanks to `Divyanshi Purohit`_ for the PR.
16
+ (:issue:`403`)
17
+ * Add :attr:`~EntryCounts.unread` property to
18
+ :class:`EntryCounts` and :class:`EntrySearchCounts`.
19
+ (:issue:`283`)
20
+
21
+ * Support Python 3.15. (:issue:`418`)
22
+ * Support PyPy 3.12. (:issue:`419`)
23
+ * Drop Python 3.11 support. (:issue:`419`)
24
+
25
+ * Stop vendoring `feedparser`_ and
26
+ :ref:`recommend installing it from GitHub instead <no-vendored-feedparser>`.
27
+ * Pin GitHub Actions and add zizmor to CI.
28
+
29
+ .. _Divyanshi Purohit: https://github.com/divyanshi555
30
+
31
+
32
+ Version 3.26
33
+ ------------
34
+
35
+ Released 2026-06-24
36
+
37
+ * Allow exporting the entire database (e.g. for backup or migration).
38
+
39
+ * Through the :meth:`~reader._types.StorageType.export` storage method.
40
+ * In the web app, on the settings page; :ref:`screenshots <screenshots-export>`.
41
+ * In the CLI, as a new ``export`` command.
42
+
43
+
9
44
  Version 3.25
10
45
  ------------
11
46
 
@@ -15,7 +50,8 @@ Released 2026-05-25
15
50
 
16
51
  * Add :mod:`~reader.plugins.autodiscover` built-in plugin.
17
52
  * Add :mod:`reader.discover` module providing low level discovery.
18
- * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions when adding a feed.
53
+ * In the web app, show :mod:`~reader.plugins.autodiscover` suggestions
54
+ when adding a feed; :ref:`screenshots <screenshots-autodiscovery>`.
19
55
 
20
56
  * Fix bugs that may cause the 3.24 database migration to fail. (:issue:`405`)
21
57
 
@@ -579,7 +615,7 @@ Released 2023-08-20
579
615
 
580
616
  * Make :exc:`ReaderWarning` inherit from :exc:`ReaderError`.
581
617
 
582
- * Include a diagram of the :ref:`exctree` in the :doc:`api`.
618
+ * Include a diagram of the :ref:`exception hierarchy <exctree>` in the :doc:`api`.
583
619
 
584
620
  * Add werkzeug dependency,
585
621
  instead of vendoring selected :mod:`werkzeug.http` utilities.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.25
3
+ Version: 3.27
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -19,10 +19,10 @@ Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Python :: 3
21
21
  Classifier: Programming Language :: Python :: 3 :: Only
22
- Classifier: Programming Language :: Python :: 3.11
23
22
  Classifier: Programming Language :: Python :: 3.12
24
23
  Classifier: Programming Language :: Python :: 3.13
25
24
  Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
26
26
  Classifier: Programming Language :: Python :: Implementation :: CPython
27
27
  Classifier: Programming Language :: Python :: Implementation :: PyPy
28
28
  Classifier: Topic :: Internet
@@ -32,7 +32,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
32
32
  Classifier: Topic :: Software Development :: Libraries
33
33
  Classifier: Topic :: Utilities
34
34
  Classifier: Typing :: Typed
35
- Requires-Python: >=3.11
35
+ Requires-Python: >=3.12
36
36
  Description-Content-Type: text/x-rst
37
37
  License-File: LICENSE
38
38
  Requires-Dist: typing-extensions>=4
@@ -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"
@@ -60,9 +60,11 @@ Data objects
60
60
 
61
61
  .. autoclass:: EntryCounts
62
62
  :members:
63
+ :inherited-members:
63
64
 
64
65
  .. autoclass:: EntrySearchCounts
65
66
  :members:
67
+ :inherited-members:
66
68
 
67
69
  .. autoclass:: UpdateResult
68
70
  :members:
@@ -85,9 +87,16 @@ Data objects
85
87
 
86
88
 
87
89
 
90
+ .. _exctree:
91
+
88
92
  Exceptions
89
93
  ----------
90
94
 
95
+ The class hierarchy for :mod:`reader` exceptions is:
96
+
97
+ .. classtree:: ReaderError
98
+
99
+
91
100
  .. autoexception:: ReaderError
92
101
  :members:
93
102
 
@@ -180,16 +189,6 @@ Exceptions
180
189
  :show-inheritance:
181
190
 
182
191
 
183
- .. _exctree:
184
-
185
- Exception hierarchy
186
- ~~~~~~~~~~~~~~~~~~~
187
-
188
- The class hierarchy for :mod:`reader` exceptions is:
189
-
190
- .. classtree:: ReaderError
191
-
192
-
193
192
 
194
193
  Enumerations
195
194
  ------------
@@ -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
  -------------------
@@ -78,7 +78,7 @@ autodoc_typehints_description_target = 'documented'
78
78
  autodoc_type_aliases = {
79
79
  'ResourceInput': 'reader.types.ResourceInput',
80
80
  'AnyResourceInput': 'reader.types.AnyResourceInput',
81
- 'JSONType': 'reader.types.JSONType',
81
+ 'JSON': 'reader.types.JSON',
82
82
  'TagFilter': 'reader._types.TagFilter',
83
83
  'TristateFilter': 'reader._types.TristateFilter',
84
84
  }
@@ -226,5 +226,19 @@ def class_tree(cls):
226
226
  return '\n'.join(output(classes)) + '\n'
227
227
 
228
228
 
229
+ import builtins
230
+
231
+ std_dir = builtins.dir
232
+
233
+
234
+ def source_order_dir(obj):
235
+ if isinstance(obj, type):
236
+ return [name for cls in obj.mro() for name in cls.__dict__.keys()]
237
+ return std_dir(obj)
238
+
239
+
229
240
  def setup(app):
230
241
  app.add_directive("classtree", ClassTree)
242
+
243
+ # TODO: remove when https://github.com/sphinx-doc/sphinx/issues/628 is resolved
244
+ builtins.dir = source_order_dir
@@ -137,17 +137,30 @@ since each connection would be to a *different* database::
137
137
  .. _temporary: https://sqlite.org/inmemorydb.html#temp_db
138
138
 
139
139
 
140
+ Migrations
141
+ ~~~~~~~~~~
142
+
143
+ Schema migrations depend on the storage used.
144
+
145
+ For the default SQLite storage,
146
+ migrations happen automatically during :func:`make_reader`,
147
+ unless you pass ``migrate=False``.
148
+
149
+
140
150
  .. _backups:
141
151
 
142
- Back-ups
143
- ~~~~~~~~
152
+ Backups
153
+ ~~~~~~~
154
+
155
+ Making backups generally depends on the storage used.
144
156
 
145
- Making back-ups depends on the storage used.
157
+ If the storage implements it, you can use
158
+ the :meth:`~reader._types.StorageType.export` method
159
+ or the ``export`` :doc:`CLI <cli>` command.
146
160
 
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).
161
+ For the SQLite storage, you can also use
162
+ the `.backup command <https://sqlite.org/cli.html>`_
163
+ or `VACUUM INTO <https://www.sqlite.org/lang_vacuum.html>`_.
151
164
 
152
165
 
153
166
 
@@ -28,14 +28,12 @@ These packages will be installed automatically when installing *reader*:
28
28
 
29
29
  .. note::
30
30
 
31
- Because `feedparser`_ makes PyPI releases at a lower cadence,
32
- *reader* uses a vendored version of feedparser's `develop`_ branch
33
- by default since :ref:`version 2.9`.
34
- To opt out of this behavior, and make *reader* use
35
- the installed ``feedparser`` package,
36
- set the ``READER_NO_VENDORED_FEEDPARSER`` environment variable to ``1``.
37
-
38
- .. _develop: https://github.com/kurtmckee/feedparser
31
+ The `feedparser`_ ``main`` branch contains a `change`_
32
+ that decreases memory usage by ~35% during feed updates;
33
+ as of feedparser 6.0.14, this change is not yet included in PyPI releases;
34
+ to benefit from it, install feedparser directly from GitHub.
35
+
36
+ .. _change: https://github.com/kurtmckee/feedparser/pull/302
39
37
 
40
38
 
41
39
  .. _optional dependencies:
@@ -87,7 +87,7 @@ Data objects
87
87
  .. autoclass:: ParsedFeed
88
88
  :members:
89
89
 
90
- .. autoclass:: HTTPInfo
90
+ .. autoclass:: RetrieveMetadata
91
91
  :members:
92
92
 
93
93
 
@@ -19,10 +19,10 @@ classifiers = [
19
19
  "Programming Language :: Python",
20
20
  "Programming Language :: Python :: 3",
21
21
  "Programming Language :: Python :: 3 :: Only",
22
- "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
25
24
  "Programming Language :: Python :: 3.14",
25
+ "Programming Language :: Python :: 3.15",
26
26
  "Programming Language :: Python :: Implementation :: CPython",
27
27
  "Programming Language :: Python :: Implementation :: PyPy",
28
28
  "Topic :: Internet",
@@ -33,7 +33,7 @@ classifiers = [
33
33
  "Topic :: Utilities",
34
34
  "Typing :: Typed",
35
35
  ]
36
- requires-python = ">=3.11"
36
+ requires-python = ">=3.12"
37
37
  dependencies = [
38
38
  "typing-extensions>=4",
39
39
  "feedparser>=6",
@@ -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",
@@ -173,6 +176,9 @@ files = ["src"]
173
176
  show_error_codes = true
174
177
  pretty = true
175
178
  strict = true
179
+ strict_equality_for_none = true
180
+ warn_unreachable = true
181
+
176
182
  [[tool.mypy.overrides]]
177
183
  module = [
178
184
  "reader.__main__",
@@ -187,7 +193,6 @@ module = [
187
193
  "reader.plugins.readtime",
188
194
  "reader.plugins.mark_as_read",
189
195
  "reader.plugins.autodiscover",
190
- "reader._vendor.feedparser.*",
191
196
  ]
192
197
  ignore_errors = true
193
198
 
@@ -20,18 +20,8 @@ def run(*args, **kwargs):
20
20
  return subprocess.run(*args, check=True, **kwargs)
21
21
 
22
22
 
23
- def run_no_venv(*args, **kwargs):
24
- env = os.environ.copy()
25
- virtual_env = env.pop('VIRTUAL_ENV', None)
26
- if virtual_env:
27
- env['PATH'] = ':'.join(
28
- p for p in env['PATH'].split(':') if not p.startswith(virtual_env)
29
- )
30
- return run(*args, env=env, **kwargs)
31
-
32
-
33
23
  def run_tox():
34
- run_no_venv('tox -p all', shell=True)
24
+ run('tox -p all', shell=True)
35
25
 
36
26
 
37
27
  def path_sub(pattern, repl, path):
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.25'
40
+ __version__ = '3.27'
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" %}