reader 3.12__tar.gz → 3.14__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 (230) hide show
  1. {reader-3.12 → reader-3.14}/CHANGES.rst +88 -1
  2. {reader-3.12/src/reader.egg-info → reader-3.14}/PKG-INFO +6 -5
  3. {reader-3.12 → reader-3.14}/docs/api.rst +3 -0
  4. {reader-3.12 → reader-3.14}/docs/cli.rst +4 -2
  5. {reader-3.12 → reader-3.14}/docs/conf.py +3 -0
  6. {reader-3.12 → reader-3.14}/docs/dev.rst +15 -3
  7. {reader-3.12 → reader-3.14}/docs/guide.rst +114 -49
  8. {reader-3.12 → reader-3.14}/docs/install.rst +1 -3
  9. {reader-3.12 → reader-3.14}/docs/internal.rst +11 -3
  10. reader-3.14/examples/custom_headers.py +50 -0
  11. {reader-3.12 → reader-3.14}/examples/parser_only.py +2 -1
  12. {reader-3.12 → reader-3.14}/examples/terminal.py +1 -0
  13. {reader-3.12 → reader-3.14}/pyproject.toml +21 -7
  14. {reader-3.12 → reader-3.14}/scripts/bench.py +3 -2
  15. {reader-3.12 → reader-3.14}/scripts/debug_storage_stats.py +2 -1
  16. {reader-3.12 → reader-3.14}/scripts/generate_import_all.py +1 -0
  17. {reader-3.12 → reader-3.14}/scripts/jscontrols.py +3 -1
  18. {reader-3.12 → reader-3.14}/src/reader/__init__.py +3 -1
  19. {reader-3.12 → reader-3.14}/src/reader/__main__.py +1 -0
  20. {reader-3.12 → reader-3.14}/src/reader/_app/__init__.py +27 -8
  21. {reader-3.12 → reader-3.14}/src/reader/_app/api_thing.py +1 -0
  22. {reader-3.12 → reader-3.14}/src/reader/_app/cli.py +1 -0
  23. {reader-3.12 → reader-3.14}/src/reader/_app/wsgi.py +1 -0
  24. {reader-3.12 → reader-3.14}/src/reader/_cli.py +15 -5
  25. {reader-3.12 → reader-3.14}/src/reader/_config.py +3 -1
  26. {reader-3.12 → reader-3.14}/src/reader/_hash_utils.py +1 -0
  27. {reader-3.12 → reader-3.14}/src/reader/_parser/__init__.py +2 -7
  28. {reader-3.12 → reader-3.14}/src/reader/_parser/_http_utils.py +2 -0
  29. {reader-3.12 → reader-3.14}/src/reader/_parser/_lazy.py +25 -26
  30. {reader-3.12 → reader-3.14}/src/reader/_parser/_url_utils.py +16 -4
  31. {reader-3.12 → reader-3.14}/src/reader/_parser/feedparser.py +1 -1
  32. {reader-3.12 → reader-3.14}/src/reader/_parser/file.py +1 -2
  33. {reader-3.12 → reader-3.14}/src/reader/_parser/http.py +1 -2
  34. {reader-3.12 → reader-3.14}/src/reader/_parser/requests/__init__.py +5 -3
  35. {reader-3.12 → reader-3.14}/src/reader/_plugins/cli_status.py +70 -46
  36. reader-3.14/src/reader/_plugins/enclosure_tags.py +166 -0
  37. {reader-3.12 → reader-3.14}/src/reader/_plugins/preview_feed_list.py +1 -0
  38. {reader-3.12 → reader-3.14}/src/reader/_plugins/share.py +1 -0
  39. {reader-3.12 → reader-3.14}/src/reader/_plugins/sqlite_releases.py +2 -0
  40. {reader-3.12 → reader-3.14}/src/reader/_plugins/timer.py +2 -1
  41. {reader-3.12 → reader-3.14}/src/reader/_storage/_base.py +6 -4
  42. {reader-3.12 → reader-3.14}/src/reader/_storage/_changes.py +1 -0
  43. {reader-3.12 → reader-3.14}/src/reader/_storage/_entries.py +34 -22
  44. {reader-3.12 → reader-3.14}/src/reader/_storage/_feeds.py +46 -87
  45. {reader-3.12 → reader-3.14}/src/reader/_storage/_html_utils.py +2 -0
  46. {reader-3.12 → reader-3.14}/src/reader/_storage/_schema.py +11 -2
  47. {reader-3.12 → reader-3.14}/src/reader/_storage/_search.py +36 -99
  48. {reader-3.12 → reader-3.14}/src/reader/_storage/_sql_utils.py +3 -0
  49. {reader-3.12 → reader-3.14}/src/reader/_storage/_sqlite_utils.py +5 -3
  50. {reader-3.12 → reader-3.14}/src/reader/_storage/_tags.py +1 -0
  51. {reader-3.12 → reader-3.14}/src/reader/_types.py +56 -35
  52. {reader-3.12 → reader-3.14}/src/reader/_update.py +196 -155
  53. {reader-3.12 → reader-3.14}/src/reader/_utils.py +3 -1
  54. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/__init__.py +29 -10
  55. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/api.py +106 -135
  56. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/__init__.py +3 -2
  57. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/asctime.py +26 -17
  58. reader-3.14/src/reader/_vendor/feedparser/datetimes/greek.py +90 -0
  59. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/hungarian.py +19 -25
  60. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/iso8601.py +62 -52
  61. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/korean.py +35 -24
  62. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/perforce.py +21 -4
  63. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/rfc822.py +52 -23
  64. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +36 -22
  65. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/encodings.py +215 -147
  66. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/exceptions.py +6 -6
  67. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/html.py +93 -84
  68. reader-3.14/src/reader/_vendor/feedparser/http.py +78 -0
  69. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/mixin.py +330 -275
  70. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/_base.py +210 -166
  71. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/admin.py +10 -10
  72. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/cc.py +20 -19
  73. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/dc.py +21 -17
  74. reader-3.14/src/reader/_vendor/feedparser/namespaces/georss.py +683 -0
  75. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/itunes.py +25 -21
  76. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/mediarss.py +37 -36
  77. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/psc.py +13 -13
  78. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/parsers/json.py +54 -52
  79. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/parsers/loose.py +27 -29
  80. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/parsers/strict.py +34 -28
  81. reader-3.14/src/reader/_vendor/feedparser/sanitizer.py +974 -0
  82. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/sgml.py +17 -17
  83. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/urls.py +91 -72
  84. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/util.py +40 -40
  85. {reader-3.12 → reader-3.14}/src/reader/core.py +64 -8
  86. {reader-3.12 → reader-3.14}/src/reader/exceptions.py +3 -2
  87. {reader-3.12 → reader-3.14}/src/reader/plugins/__init__.py +2 -0
  88. {reader-3.12 → reader-3.14}/src/reader/plugins/enclosure_dedupe.py +1 -0
  89. {reader-3.12 → reader-3.14}/src/reader/plugins/entry_dedupe.py +45 -9
  90. {reader-3.12 → reader-3.14}/src/reader/plugins/mark_as_read.py +29 -0
  91. {reader-3.12 → reader-3.14}/src/reader/plugins/readtime.py +2 -0
  92. {reader-3.12 → reader-3.14}/src/reader/plugins/ua_fallback.py +2 -0
  93. {reader-3.12 → reader-3.14}/src/reader/types.py +72 -22
  94. {reader-3.12 → reader-3.14/src/reader.egg-info}/PKG-INFO +6 -5
  95. {reader-3.12 → reader-3.14}/src/reader.egg-info/SOURCES.txt +2 -0
  96. {reader-3.12 → reader-3.14}/src/reader.egg-info/requires.txt +3 -3
  97. {reader-3.12 → reader-3.14}/tests/conftest.py +7 -24
  98. {reader-3.12 → reader-3.14}/tests/test_app.py +3 -2
  99. {reader-3.12 → reader-3.14}/tests/test_bench.py +4 -7
  100. {reader-3.12 → reader-3.14}/tests/test_cli.py +18 -2
  101. {reader-3.12 → reader-3.14}/tests/test_lazy_imports.py +2 -1
  102. {reader-3.12 → reader-3.14}/tests/test_parser.py +7 -4
  103. reader-3.14/tests/test_plugins_cli_status.py +105 -0
  104. {reader-3.12 → reader-3.14}/tests/test_plugins_enclosure_dedupe.py +1 -2
  105. {reader-3.12 → reader-3.14}/tests/test_plugins_entry_dedupe.py +41 -7
  106. {reader-3.12 → reader-3.14}/tests/test_plugins_mark_as_read.py +47 -0
  107. {reader-3.12 → reader-3.14}/tests/test_reader.py +432 -67
  108. {reader-3.12 → reader-3.14}/tests/test_reader_context.py +3 -3
  109. {reader-3.12 → reader-3.14}/tests/test_reader_counts.py +44 -6
  110. {reader-3.12 → reader-3.14}/tests/test_reader_deprecations.py +1 -0
  111. {reader-3.12 → reader-3.14}/tests/test_reader_filter.py +7 -4
  112. {reader-3.12 → reader-3.14}/tests/test_reader_integration.py +27 -4
  113. {reader-3.12 → reader-3.14}/tests/test_reader_search.py +8 -8
  114. {reader-3.12 → reader-3.14}/tests/test_reader_sort.py +1 -0
  115. reader-3.14/tests/test_reader_update.py +163 -0
  116. {reader-3.12 → reader-3.14}/tests/test_search.py +3 -4
  117. {reader-3.12 → reader-3.14}/tests/test_storage.py +11 -14
  118. {reader-3.12 → reader-3.14}/tests/test_test_utils.py +5 -1
  119. {reader-3.12 → reader-3.14}/tests/test_utils.py +1 -0
  120. {reader-3.12 → reader-3.14}/tests/utils.py +9 -4
  121. {reader-3.12 → reader-3.14}/tox.ini +3 -3
  122. reader-3.12/src/reader/_plugins/enclosure_tags.py +0 -117
  123. reader-3.12/src/reader/_vendor/feedparser/datetimes/greek.py +0 -86
  124. reader-3.12/src/reader/_vendor/feedparser/http.py +0 -227
  125. reader-3.12/src/reader/_vendor/feedparser/namespaces/georss.py +0 -276
  126. reader-3.12/src/reader/_vendor/feedparser/sanitizer.py +0 -950
  127. reader-3.12/tests/test_plugins_cli_status.py +0 -26
  128. {reader-3.12 → reader-3.14}/LICENSE +0 -0
  129. {reader-3.12 → reader-3.14}/MANIFEST.in +0 -0
  130. {reader-3.12 → reader-3.14}/README.rst +0 -0
  131. {reader-3.12 → reader-3.14}/docs/Makefile +0 -0
  132. {reader-3.12 → reader-3.14}/docs/app.rst +0 -0
  133. {reader-3.12 → reader-3.14}/docs/changelog.rst +0 -0
  134. {reader-3.12 → reader-3.14}/docs/config.rst +0 -0
  135. {reader-3.12 → reader-3.14}/docs/contributing.rst +0 -0
  136. {reader-3.12 → reader-3.14}/docs/dev-app.rst +0 -0
  137. {reader-3.12 → reader-3.14}/docs/images/redesign-01.png +0 -0
  138. {reader-3.12 → reader-3.14}/docs/index.rst +0 -0
  139. {reader-3.12 → reader-3.14}/docs/make.bat +0 -0
  140. {reader-3.12 → reader-3.14}/docs/plugins.rst +0 -0
  141. {reader-3.12 → reader-3.14}/docs/screenshots/dillo.png +0 -0
  142. {reader-3.12 → reader-3.14}/docs/screenshots/entries-feed.png +0 -0
  143. {reader-3.12 → reader-3.14}/docs/screenshots/entries.png +0 -0
  144. {reader-3.12 → reader-3.14}/docs/screenshots/entry-one.png +0 -0
  145. {reader-3.12 → reader-3.14}/docs/screenshots/entry-two.png +0 -0
  146. {reader-3.12 → reader-3.14}/docs/screenshots/feeds.png +0 -0
  147. {reader-3.12 → reader-3.14}/docs/screenshots/lynx.png +0 -0
  148. {reader-3.12 → reader-3.14}/docs/screenshots/search.png +0 -0
  149. {reader-3.12 → reader-3.14}/docs/tutorial.rst +0 -0
  150. {reader-3.12 → reader-3.14}/docs/why.rst +0 -0
  151. {reader-3.12 → reader-3.14}/examples/config.yaml +0 -0
  152. {reader-3.12 → reader-3.14}/examples/podcast.py +0 -0
  153. {reader-3.12 → reader-3.14}/run.sh +0 -0
  154. {reader-3.12 → reader-3.14}/scripts/.coverage +0 -0
  155. {reader-3.12 → reader-3.14}/scripts/backup.sh +0 -0
  156. {reader-3.12 → reader-3.14}/scripts/jscontrols.html +0 -0
  157. {reader-3.12 → reader-3.14}/scripts/lines.sh +0 -0
  158. {reader-3.12 → reader-3.14}/scripts/release.py +0 -0
  159. {reader-3.12 → reader-3.14}/setup.cfg +0 -0
  160. {reader-3.12 → reader-3.14}/src/reader/_app/static/controls.js +0 -0
  161. {reader-3.12 → reader-3.14}/src/reader/_app/static/style.css +0 -0
  162. {reader-3.12 → reader-3.14}/src/reader/_app/templates/add_entry.html +0 -0
  163. {reader-3.12 → reader-3.14}/src/reader/_app/templates/entries.html +0 -0
  164. {reader-3.12 → reader-3.14}/src/reader/_app/templates/entry.html +0 -0
  165. {reader-3.12 → reader-3.14}/src/reader/_app/templates/feeds.html +0 -0
  166. {reader-3.12 → reader-3.14}/src/reader/_app/templates/layout.html +0 -0
  167. {reader-3.12 → reader-3.14}/src/reader/_app/templates/macros.html +0 -0
  168. {reader-3.12 → reader-3.14}/src/reader/_app/templates/metadata.html +0 -0
  169. {reader-3.12 → reader-3.14}/src/reader/_app/templates/tags.html +0 -0
  170. {reader-3.12 → reader-3.14}/src/reader/_parser/jsonfeed.py +1 -1
  171. {reader-3.12 → reader-3.14}/src/reader/_parser/requests/_lazy.py +2 -2
  172. {reader-3.12 → reader-3.14}/src/reader/_plugins/__init__.py +1 -1
  173. {reader-3.12 → reader-3.14}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  174. {reader-3.12 → reader-3.14}/src/reader/_storage/__init__.py +0 -0
  175. {reader-3.12 → reader-3.14}/src/reader/_vendor/__init__.py +0 -0
  176. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  177. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  178. {reader-3.12 → reader-3.14}/src/reader/_vendor/feedparser/py.typed +0 -0
  179. {reader-3.12 → reader-3.14}/src/reader/py.typed +0 -0
  180. {reader-3.12 → reader-3.14}/src/reader.egg-info/dependency_links.txt +0 -0
  181. {reader-3.12 → reader-3.14}/src/reader.egg-info/top_level.txt +0 -0
  182. {reader-3.12 → reader-3.14}/tests/data/10.json +0 -0
  183. {reader-3.12 → reader-3.14}/tests/data/10.json.py +0 -0
  184. {reader-3.12 → reader-3.14}/tests/data/custom +0 -0
  185. {reader-3.12 → reader-3.14}/tests/data/empty.atom +0 -0
  186. {reader-3.12 → reader-3.14}/tests/data/empty.atom.py +0 -0
  187. {reader-3.12 → reader-3.14}/tests/data/empty.json +0 -0
  188. {reader-3.12 → reader-3.14}/tests/data/empty.json.py +0 -0
  189. {reader-3.12 → reader-3.14}/tests/data/empty.rss +0 -0
  190. {reader-3.12 → reader-3.14}/tests/data/empty.rss.py +0 -0
  191. {reader-3.12 → reader-3.14}/tests/data/full.atom +0 -0
  192. {reader-3.12 → reader-3.14}/tests/data/full.atom.py +0 -0
  193. {reader-3.12 → reader-3.14}/tests/data/full.json +0 -0
  194. {reader-3.12 → reader-3.14}/tests/data/full.json.py +0 -0
  195. {reader-3.12 → reader-3.14}/tests/data/full.rss +0 -0
  196. {reader-3.12 → reader-3.14}/tests/data/full.rss.py +0 -0
  197. {reader-3.12 → reader-3.14}/tests/data/invalid.json +0 -0
  198. {reader-3.12 → reader-3.14}/tests/data/invalid.json.py +0 -0
  199. {reader-3.12 → reader-3.14}/tests/data/relative.atom +0 -0
  200. {reader-3.12 → reader-3.14}/tests/data/relative.atom.py +0 -0
  201. {reader-3.12 → reader-3.14}/tests/data/relative.rss +0 -0
  202. {reader-3.12 → reader-3.14}/tests/data/relative.rss.py +0 -0
  203. {reader-3.12 → reader-3.14}/tests/data/sqlite_releases.html +0 -0
  204. {reader-3.12 → reader-3.14}/tests/data/unknown.json +0 -0
  205. {reader-3.12 → reader-3.14}/tests/data/unknown.json.py +0 -0
  206. {reader-3.12 → reader-3.14}/tests/fakeparser.py +0 -0
  207. {reader-3.12 → reader-3.14}/tests/reader_methods.py +0 -0
  208. {reader-3.12 → reader-3.14}/tests/reader_test_plugins/__init__.py +0 -0
  209. {reader-3.12 → reader-3.14}/tests/reader_test_plugins/good.py +0 -0
  210. {reader-3.12 → reader-3.14}/tests/reader_test_plugins/init_error.py +0 -0
  211. {reader-3.12 → reader-3.14}/tests/reader_test_plugins/missing_dependency.py +0 -0
  212. {reader-3.12 → reader-3.14}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  213. {reader-3.12 → reader-3.14}/tests/test__types.py +0 -0
  214. {reader-3.12 → reader-3.14}/tests/test_app_wsgi.py +1 -1
  215. {reader-3.12 → reader-3.14}/tests/test_changes.py +1 -1
  216. {reader-3.12 → reader-3.14}/tests/test_config.py +0 -0
  217. {reader-3.12 → reader-3.14}/tests/test_exceptions.py +0 -0
  218. {reader-3.12 → reader-3.14}/tests/test_hash_utils.py +0 -0
  219. {reader-3.12 → reader-3.14}/tests/test_html_utils.py +0 -0
  220. {reader-3.12 → reader-3.14}/tests/test_plugins_preview_feed_list.py +2 -2
  221. {reader-3.12 → reader-3.14}/tests/test_plugins_readtime.py +3 -3
  222. {reader-3.12 → reader-3.14}/tests/test_plugins_sqlite_releases.py +1 -1
  223. {reader-3.12 → reader-3.14}/tests/test_plugins_ua_fallback.py +0 -0
  224. {reader-3.12 → reader-3.14}/tests/test_reader_hooks.py +4 -4
  225. {reader-3.12 → reader-3.14}/tests/test_reader_plugins.py +0 -0
  226. {reader-3.12 → reader-3.14}/tests/test_reader_private.py +3 -3
  227. {reader-3.12 → reader-3.14}/tests/test_sql_utils.py +0 -0
  228. {reader-3.12 → reader-3.14}/tests/test_sqlite_utils.py +1 -1
  229. {reader-3.12 → reader-3.14}/tests/test_tags.py +2 -2
  230. {reader-3.12 → reader-3.14}/tests/test_types.py +0 -0
@@ -6,6 +6,93 @@ Changelog
6
6
  :noindex:
7
7
 
8
8
 
9
+ Version 3.14
10
+ ------------
11
+
12
+ Released 2024-07-23
13
+
14
+ * Add :attr:`~EntryCounts.unimportant` attribute to
15
+ :class:`EntryCounts` and :class:`EntrySearchCounts`.
16
+ Thanks to `chenthur`_ for the pull request.
17
+ (:issue:`283`)
18
+ * Fix bug in the :mod:`~reader.plugins.entry_dedupe` plugin causing entries
19
+ to flip-flop if there were multiple *new* duplicates of the same issue
20
+ (on the first update, one entry remains, on the second update, the other);
21
+ related to the bug fixed in `version 3.2 <Version 3.2_>`_.
22
+ (:issue:`340`)
23
+
24
+ * :mod:`~reader._plugins.enclosure_tags` plugin improvements. (:issue:`344`)
25
+
26
+ * Set genre to `Podcast` if the feed has any tag containing "podcast".
27
+ * Rewrite tags on the fly,
28
+ instead of downloading the entire file before sending it to the user;
29
+ allows browsers to display accurate download progress.
30
+ * Prefer feed user title to feed title if available.
31
+ * Use feed title as artist, instead of author.
32
+
33
+ * Support Python 3.13. (:issue:`341`)
34
+ * Update vendored `feedparser`_ to the ``develop`` branch as of 2024-06-26.
35
+ Needed because upstream removed dependency on stdlib module ``cgi``
36
+ (removed in Python 3.13), but the `version 2.9 <Version 2.9_>`_
37
+ memory usage reduction was not released upstream yet.
38
+ (:issue:`341`)
39
+
40
+ .. _chenthur: https://github.com/chenthur
41
+ .. _feedparser: https://feedparser.readthedocs.io/en/latest/
42
+
43
+
44
+ Version 3.13
45
+ ------------
46
+
47
+ Released 2024-06-19
48
+
49
+ * Allow different feed update intervals; see :ref:`scheduled` for details.
50
+ (:issue:`332`)
51
+
52
+ * Add ``scheduled`` argument to :meth:`~Reader.update_feeds()`,
53
+ :meth:`~Reader.get_feeds()`, etc.
54
+
55
+ * Add :attr:`Feed.update_after` and :attr:`~Feed.last_retrieved` attributes.
56
+
57
+ * The ``new`` filter of :meth:`~Reader.update_feeds()` etc. considers
58
+ a feed new if it was never retrieved (:attr:`~Feed.last_retrieved`),
59
+ not if it was never updated successfully (:attr:`~Feed.last_updated`).
60
+
61
+ * Update an entry whenever its :attr:`~Entry.updated` changes
62
+ (don't require it to be newer).
63
+
64
+ * Fix bug introduced in `version 3.12 <Version 3.12_>`_ causing an assertion error
65
+ when there are multiple entries with the same id in the same feed,
66
+ or when parallel :meth:`~Reader.update_feeds` calls add the same entry.
67
+ The fix restores the pre-3.12 first-entry-wins / last-write-wins behavior.
68
+ Thanks to `Joakim Hellsén`_ for reporting and helping debug this issue.
69
+ (:issue:`335`)
70
+
71
+ * Fix assertion error when an entry is deleted while being updated.
72
+
73
+ * Allow re-running the :mod:`~reader.plugins.mark_as_read` plugin for existing entries.
74
+ Thanks to `Michael Han`_ for the pull request.
75
+ (:issue:`317`)
76
+
77
+ * Other changes for scheduled updates. (:issue:`332`)
78
+
79
+ * Add ``--scheduled`` flag to the ``update`` command.
80
+ * The :mod:`~reader._plugins.cli_status` plugin now
81
+ records the output of multiple runs instead of just the last one,
82
+ with output from the same hour grouped in a single entry,
83
+ up to 24 entries/hours.
84
+ * Group mutually-exclusive attributes of :class:`~.FeedUpdateIntent`
85
+ into a new :attr:`~.FeedUpdateIntent.value` union attribute.
86
+
87
+ * New and improved :ref:`update` user guide section.
88
+ * Rename ``update --new-only`` CLI flag to ``--new``;
89
+ ``--new-only`` remains available as an alias.
90
+ (:issue:`334`)
91
+
92
+ .. _Joakim Hellsén: https://github.com/TheLovinator1
93
+ .. _Michael Han: https://github.com/Sakunam
94
+
95
+
9
96
  Version 3.12
10
97
  ------------
11
98
 
@@ -289,7 +376,7 @@ Released 2022-09-14
289
376
  became optional.
290
377
  (:issue:`96`)
291
378
  * Fix bug in :mod:`~reader.plugins.entry_dedupe` causing updates to fail
292
- if there were multiple *new* duplicates of the same issue.
379
+ if there were multiple *new* duplicates of the same entry.
293
380
  (:issue:`292`)
294
381
  * Fix bug in :mod:`~reader.plugins.readtime`
295
382
  and :mod:`~reader.plugins.mark_as_read` causing updates to fail
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.12
3
+ Version: 3.14
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
22
22
  Classifier: Programming Language :: Python :: 3.10
23
23
  Classifier: Programming Language :: Python :: 3.11
24
24
  Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
25
26
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
27
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
28
  Classifier: Topic :: Internet
@@ -61,15 +62,15 @@ Provides-Extra: tests
61
62
  Requires-Dist: pytest>=4; extra == "tests"
62
63
  Requires-Dist: pytest-randomly; extra == "tests"
63
64
  Requires-Dist: pytest-subtests; extra == "tests"
64
- Requires-Dist: flaky; extra == "tests"
65
+ Requires-Dist: pytest-rerunfailures; extra == "tests"
65
66
  Requires-Dist: coverage; extra == "tests"
66
67
  Requires-Dist: pytest-cov; extra == "tests"
67
68
  Requires-Dist: requests-mock; extra == "tests"
68
- Requires-Dist: mechanicalsoup; (implementation_name != "pypy" or python_version <= "3.9") and extra == "tests"
69
+ Requires-Dist: mechanicalsoup; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
69
70
  Requires-Dist: requests-wsgi-adapter; extra == "tests"
70
- Requires-Dist: lxml; (implementation_name != "pypy" or python_version <= "3.9") and extra == "tests"
71
+ Requires-Dist: lxml; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
71
72
  Requires-Dist: html5lib; extra == "tests"
72
- Requires-Dist: numpy; (implementation_name != "pypy" and os_name == "posix") and extra == "tests"
73
+ Requires-Dist: numpy; (implementation_name != "pypy" and os_name == "posix" and python_version <= "3.12") and extra == "tests"
73
74
  Requires-Dist: mypy; implementation_name != "pypy" and extra == "tests"
74
75
  Requires-Dist: types-requests; extra == "tests"
75
76
  Requires-Dist: types-beautifulsoup4; extra == "tests"
@@ -176,6 +176,9 @@ Type aliases
176
176
  .. autodata:: reader.types.TagFilterInput
177
177
  .. autodata:: reader.types.TristateFilterInput
178
178
 
179
+ .. autoclass:: reader.types.UpdateConfig
180
+ :members:
181
+
179
182
 
180
183
 
181
184
  Constants
@@ -45,6 +45,8 @@ Serve the web application locally (at http://localhost:8080/):
45
45
  python -m reader serve
46
46
 
47
47
 
48
+ .. _cli-update:
49
+
48
50
  Updating feeds
49
51
  --------------
50
52
 
@@ -56,13 +58,13 @@ hour). Note that *reader* uses the ETag and Last-Modified headers, so, if
56
58
  supported by the the server, feeds will only be downloaded if they changed.
57
59
 
58
60
  To avoid waiting too much for a new feed to be updated, you can run
59
- ``update --new-only`` more often (e.g. every minute); this will update
61
+ ``update --new`` more often (e.g. every minute); this will update
60
62
  only newly-added feeds. This is also a good time to update the search index.
61
63
 
62
64
  You can achieve this using cron::
63
65
 
64
66
  42 * * * * reader update -v 2>&1 >>"/tmp/$LOGNAME.reader.update.hourly.log"
65
- * * * * * reader update -v --new-only 2>&1 >>"/tmp/$LOGNAME.reader.update.new.log"; reader search update 2>&1 >>"/tmp/$LOGNAME.reader.search.update.log"
67
+ * * * * * reader update -v --new 2>&1 >>"/tmp/$LOGNAME.reader.update.new.log"; reader search update 2>&1 >>"/tmp/$LOGNAME.reader.search.update.log"
66
68
 
67
69
  If you are running *reader* on a personal computer, it might also be convenient
68
70
  to run ``update`` once immediately after boot::
@@ -6,6 +6,7 @@ import unittest.mock
6
6
  import packaging.version
7
7
  from setuptools.config.pyprojecttoml import read_configuration
8
8
 
9
+
9
10
  sys.path.insert(0, os.path.abspath('../src'))
10
11
 
11
12
  # mock some things "by hand", so we can import reader below without any dependencies
@@ -23,6 +24,7 @@ for name in [
23
24
 
24
25
  import reader
25
26
 
27
+
26
28
  extensions = [
27
29
  'sphinx_rtd_theme',
28
30
  'sphinx.ext.autodoc',
@@ -155,6 +157,7 @@ texinfo_documents = [
155
157
 
156
158
 
157
159
  import pkgutil
160
+
158
161
  from docutils import nodes
159
162
  from sphinx.util.docutils import SphinxDirective
160
163
 
@@ -600,6 +600,8 @@ Updating entries based on a hash of their content (regardless of ``updated``):
600
600
 
601
601
  Decision to ignore feed.updated when updating feeds: :issue:`231`.
602
602
 
603
+ :ref:`scheduled` (initial design + postponed features): :issue:`322`.
604
+
603
605
 
604
606
  .. _deleting entries:
605
607
 
@@ -713,10 +715,20 @@ OPML support
713
715
  Thoughts on dynamic lists of feeds: :issue:`165#issuecomment-905893420`.
714
716
 
715
717
 
716
- entry_dedupe
717
- ~~~~~~~~~~~~
718
+ Duplicate entries
719
+ ~~~~~~~~~~~~~~~~~
720
+
721
+ Duplicate entries are mainly handled by the :mod:`reader.entry_dedupe` plugin.
722
+
723
+ * Using MinHash to speed up similarity checks (maybe): https://gist.github.com/lemon24/b9af5ade919713406bda9603847d32e5
724
+ * Discussion of unifying "on-line" dedupe (after an entry is added/updated),
725
+ and "on-demand" dedupe (backfill): :issue:`340`.
718
726
 
719
- Using MinHash to speed up similarity checks (maybe): https://gist.github.com/lemon24/b9af5ade919713406bda9603847d32e5
727
+ However, it is also possible for a feed to have two entries with the same id
728
+ – yes, even though in most (if not all) formats,
729
+ the id is meant to be *universally unique*!
730
+ As of 3.13, we do not support multiple entries with the same id
731
+ (the first entry wins); see :issue:`335` for thoughts on this.
720
732
 
721
733
 
722
734
  REST API
@@ -36,8 +36,7 @@ The default (and currently only) storage uses SQLite,
36
36
  so the path behaves like the ``database`` argument of :func:`sqlite3.connect`:
37
37
 
38
38
  * If the database does not exist, it will be created automatically.
39
- * You can pass ``":memory:"`` to use a temporary in-memory database;
40
- the data will disappear when the reader is closed.
39
+ * You can pass ``':memory:'`` to use a :ref:`temporary database <temp>`.
41
40
 
42
41
 
43
42
  .. _lifecycle:
@@ -93,11 +92,18 @@ or if the thread was not created through the :mod:`threading` module
93
92
  when garbage-collected).
94
93
 
95
94
 
95
+ .. _temp:
96
+
96
97
  Temporary databases
97
98
  ~~~~~~~~~~~~~~~~~~~
98
99
 
100
+ With the default SQLite storage,
101
+ you can use an `in-memory`_ (or `temporary`_) database
102
+ by using ``':memory:'`` (or ``''``) as the database path;
103
+ the data will disappear when the reader is closed.
104
+
99
105
  To maximize the usefulness of temporary databases,
100
- the database connection is closed (and the data discarded)
106
+ the connection is closed (and the data discarded)
101
107
  only when calling :meth:`~Reader.close`,
102
108
  not when using the reader as a context manager.
103
109
  The reader cannot be reused after calling :meth:`~Reader.close`.
@@ -127,6 +133,10 @@ since each connection would be to a *different* database::
127
133
  reader.exceptions.StorageError: usage error: cannot use a private database from threads other than the creating thread
128
134
 
129
135
 
136
+ .. _in-memory: https://sqlite.org/inmemorydb.html
137
+ .. _temporary: https://sqlite.org/inmemorydb.html#temp_db
138
+
139
+
130
140
  .. _backups:
131
141
 
132
142
  Back-ups
@@ -207,42 +217,96 @@ use :meth:`~Reader.delete_feed`::
207
217
  Updating feeds
208
218
  --------------
209
219
 
210
- To retrieve the latest version of a feed, along with any new entries,
211
- it must be updated.
212
220
  You can update all the feeds by using the :meth:`~Reader.update_feeds` method::
213
221
 
214
222
  >>> reader.update_feeds()
215
223
  >>> reader.get_feed(feed)
216
224
  Feed(url='http://www.hellointernet.fm/podcast?format=rss', updated=datetime.datetime(2020, 2, 28, 9, 34, 2, tzinfo=datetime.timezone.utc), title='Hello Internet', ...)
217
225
 
218
-
219
226
  To retrive feeds in parallel, use the ``workers`` flag::
220
227
 
221
228
  >>> reader.update_feeds(workers=10)
222
229
 
223
-
224
- You can also update a specific feed using :meth:`~Reader.update_feed`::
230
+ You can update a single feed using :meth:`~Reader.update_feed`::
225
231
 
226
232
  >>> reader.update_feed("http://www.hellointernet.fm/podcast?format=rss")
233
+ UpdatedFeed(url='http://www.hellointernet.fm/podcast?format=rss', new=100, modified=0, unmodified=0)
234
+
235
+
236
+ Saving bandwidth
237
+ ~~~~~~~~~~~~~~~~
238
+
239
+ If supported by the server,
240
+ *reader* uses the `ETag and Last-Modified headers`_
241
+ to get the entire content of a feed only if it changed.
242
+
243
+ .. important::
244
+
245
+ If you prevent *reader* from saving feed state between updates
246
+ (e.g. by using a :ref:`temporary database <temp>`,
247
+ or by deleting the database or feeds every time),
248
+ you will repeatedly download feeds that have not changed.
249
+ This wastes your bandwidth and the publisher's bandwidth,
250
+ and the publisher may ban you from accessing their server.
251
+
252
+ Even so, you should not update feeds *too* often;
253
+ every hour seems reasonable.
254
+ To update newly-added feeds as soon as they are added,
255
+ you can call :meth:`update_feeds(new=True) <Reader.update_feeds>`
256
+ more often (e.g. every minute).
257
+
258
+ .. seealso::
259
+
260
+ The :ref:`cli-update` section of :doc:`cli`
261
+ for an example of how to do this using cron.
262
+
263
+
264
+ .. _ETag and Last-Modified headers: https://feedparser.readthedocs.io/en/latest/http-etag.html
265
+
227
266
 
228
- If supported by the server, *reader* uses the ETag and Last-Modified headers
229
- to only retrieve feeds if they changed
230
- (`details <https://feedparser.readthedocs.io/en/latest/http-etag.html>`_).
231
- Even so, you should not update feeds *too* often,
232
- to avoid wasting the feed publisher's resources,
233
- and potentially getting banned;
234
- every 30 minutes seems reasonable.
267
+ .. _scheduled:
235
268
 
236
- To support updating newly-added feeds off the regular update schedule,
237
- you can use the ``new_only`` flag;
238
- you can call this more often (e.g. every minute)::
269
+ Scheduled updates
270
+ ~~~~~~~~~~~~~~~~~
239
271
 
240
- >>> reader.update_feeds(new_only=True)
272
+ Because different feeds need to be updated at different rates,
273
+ *reader* also provides a mechanism for scheduling updates.
241
274
 
275
+ Each feed has an update interval that, on every update,
276
+ determines when the feed should be updated next.
277
+ Running :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
278
+ updates only the feeds that should be updated at or before the current time.
279
+
280
+ The global and per-feed update interval can be **configured by the user**
281
+ via the ``.reader.update`` global/feed tag;
282
+ the default interval is of one hour;
283
+ see :data:`~reader.types.UpdateConfig` for the schema.
284
+ In addition to the interval, the user can specify a jitter;
285
+ for an interval of 24 hours, a jitter of 0.25 means
286
+ the update will occur any time in the first 6 hours of the interval.
287
+
288
+ .. note::
289
+
290
+ As of |version|, there is no way to specify a minimum update interval.
291
+ If you want feeds to be updated no more often than e.g. every hour,
292
+ you have to run :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
293
+ no more often than every hour.
294
+ Please :ref:`open an issue <issues>` if you need a minimum update interval.
295
+
296
+ In a future version of *reader*,
297
+ the same mechanism will be used to handle
298
+ HTTP 429 Too Many Requests; see :issue:`307` for details.
299
+
300
+
301
+ .. versionadded:: 3.13
302
+
303
+
304
+ Update status
305
+ ~~~~~~~~~~~~~
242
306
 
243
307
  If you need the status of each feed as it gets updated
244
308
  (for instance, to update a progress bar),
245
- you can use :meth:`~Reader.update_feeds_iter` instead,
309
+ you can use :meth:`~Reader.update_feeds_iter` instead of :meth:`~Reader.update_feeds`,
246
310
  and get a (url, updated feed or none or exception) pair for each feed::
247
311
 
248
312
  >>> for url, value in reader.update_feeds_iter():
@@ -257,9 +321,14 @@ and get a (url, updated feed or none or exception) pair for each feed::
257
321
  https://www.relay.fm/cortex/feed not modified
258
322
 
259
323
 
324
+ Regardless of the update method used,
325
+ :attr:`Feed.last_retrieved`, :attr:`~Feed.last_updated`,
326
+ and :attr:`~Feed.last_exception` will be set accordingly
327
+ (also see :ref:`errors`).
260
328
 
261
- Disabling feed updates
262
- ----------------------
329
+
330
+ Disabling updates
331
+ ~~~~~~~~~~~~~~~~~
263
332
 
264
333
  Sometimes, it is useful to skip a feed when using :meth:`~Reader.update_feeds`;
265
334
  for example, the feed does not exist anymore,
@@ -282,30 +351,22 @@ Getting feeds
282
351
  -------------
283
352
 
284
353
  As seen in the previous sections,
285
- :meth:`~Reader.get_feed` returns a :class:`Feed` object
286
- with more information about a feed::
354
+ :meth:`~Reader.get_feed` returns a :class:`Feed` object::
287
355
 
288
- >>> from prettyprinter import pprint, install_extras;
289
- >>> install_extras(include=['dataclasses'])
290
356
  >>> feed = reader.get_feed(feed)
291
357
  >>> pprint(feed)
292
- reader.types.Feed(
293
- url='http://www.hellointernet.fm/podcast?format=rss',
294
- updated=datetime.datetime(
295
- year=2020,
296
- month=2,
297
- day=28,
298
- hour=9,
299
- minute=34,
300
- second=2,
301
- tzinfo=datetime.timezone.utc
302
- ),
358
+ Feed(url='http://www.hellointernet.fm/podcast?format=rss',
359
+ updated=datetime.datetime(2020, 2, 28, 9, 34, 2, tzinfo=datetime.timezone.utc),
303
360
  title='Hello Internet',
304
361
  link='http://www.hellointernet.fm/',
305
362
  author='CGP Grey',
363
+ subtitle='CGP Grey and Brady Haran talk about YouTube, life, work, whatever.',
364
+ version='rss20',
365
+ user_title=None,
306
366
  added=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc),
307
- last_updated=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc)
308
- )
367
+ last_updated=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc),
368
+ last_exception=None,
369
+ updates_enabled=True)
309
370
 
310
371
  To get all the feeds, use the :meth:`~Reader.get_feeds` method::
311
372
 
@@ -355,7 +416,7 @@ You can get all the entries, most-recent first,
355
416
  by using :meth:`~Reader.get_entries()`,
356
417
  which generates :class:`Entry` objects::
357
418
 
358
- >>> for entry, _ in zip(reader.get_entries(), range(10)):
419
+ >>> for entry in reader.get_entries(limit=10):
359
420
  ... print(entry.feed.title, '-', entry.title)
360
421
  ...
361
422
  Cortex - 106: Clear and Boring
@@ -370,12 +431,11 @@ Here is an example of getting entries for a single feed::
370
431
 
371
432
  >>> feed.title
372
433
  'Hello Internet'
373
- >>> entries = list(reader.get_entries(feed=feed))
374
- >>> for entry in entries[:2]:
375
- ... print(entry.feed.title, '-', entry.title)
434
+ >>> for entry in reader.get_entries(feed=feed, limit=2):
435
+ ... print(entry.title)
376
436
  ...
377
- Hello Internet - H.I. #136: Dog Bingo
378
- Hello Internet - H.I. #135: Place Your Bets
437
+ H.I. #136: Dog Bingo
438
+ H.I. #135: Place Your Bets
379
439
 
380
440
 
381
441
 
@@ -385,9 +445,11 @@ Entry flags
385
445
  Entries can be marked as :attr:`~Entry.read` or :attr:`~Entry.important`.
386
446
  These flags can be used for filtering::
387
447
 
388
- >>> reader.mark_entry_as_read(entries[0])
389
- >>> entries = list(reader.get_entries(feed=feed, read=False))
390
- >>> for entry in entries[:2]:
448
+ >>> entry = next(reader.get_entries(feed=feed))
449
+ >>> entry.title
450
+ 'H.I. #136: Dog Bingo'
451
+ >>> reader.mark_entry_as_read(entry)
452
+ >>> for entry in reader.get_entries(feed=feed, read=False, limit=2):
391
453
  ... print(entry.title)
392
454
  ...
393
455
  H.I. #135: Place Your Bets
@@ -766,7 +828,8 @@ This applies to the following names:
766
828
  * tag keys
767
829
  * the top-level keys of dict tag values
768
830
 
769
- Currently, there are no *reader*-reserved names;
831
+ Currently, the only *reader*-reserved names
832
+ are used by `Scheduled updates`_ and by :ref:`built-in plugins`;
770
833
  new ones will be documented here.
771
834
 
772
835
  The prefixes can be changed using
@@ -894,6 +957,8 @@ depending on their type attribute or feedparser defaults:
894
957
 
895
958
 
896
959
 
960
+ .. _errors:
961
+
897
962
  Errors and exceptions
898
963
  ---------------------
899
964
 
@@ -24,9 +24,7 @@ These packages will be installed automatically when installing *reader*:
24
24
  * `typing-extensions`_ is used for :mod:`typing` backports.
25
25
 
26
26
  *reader* also depends on the :mod:`sqlite3` standard library module
27
- (at least SQLite 3.15), and on the `JSON1`_ SQLite extension.
28
- To use the :ref:`full-text search <fts>` functionality,
29
- at least SQLite 3.18 with the `FTS5`_ extension is required.
27
+ (at least SQLite 3.18 with the `JSON1`_ and `FTS5`_ extensions).
30
28
 
31
29
 
32
30
  .. _optional dependencies:
@@ -202,6 +202,9 @@ Data objects
202
202
  .. autoclass:: FeedUpdateIntent
203
203
  :members:
204
204
 
205
+ .. autoclass:: FeedToUpdate
206
+ :members:
207
+
205
208
  .. autoclass:: EntryUpdateIntent
206
209
  :members:
207
210
 
@@ -216,9 +219,14 @@ Type aliases
216
219
  Recipes
217
220
  -------
218
221
 
219
- .. include:: ../examples/parser_only.py
222
+ .. include:: ../examples/custom_headers.py
220
223
  :start-after: """
221
- :end-before: """ # docstring-end
224
+ :end-before: """
225
+ .. literalinclude:: ../examples/custom_headers.py
226
+ :start-at: import
222
227
 
228
+ .. include:: ../examples/parser_only.py
229
+ :start-after: """
230
+ :end-before: """
223
231
  .. literalinclude:: ../examples/parser_only.py
224
- :start-after: """ # docstring-end
232
+ :start-at: import
@@ -0,0 +1,50 @@
1
+ """
2
+ Adding custom headers when retrieving feeds
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Example of adding custom request headers with :attr:`.SessionFactory.request_hooks`:
6
+
7
+ .. code-block:: console
8
+
9
+ $ python examples/custom_headers.py
10
+ updating...
11
+ server: Hello, world!
12
+ updated!
13
+
14
+ """
15
+
16
+ # fmt: off
17
+ # flake8: noqa
18
+
19
+ import http.server
20
+ import threading
21
+ from reader import make_reader
22
+
23
+ # start a background server that logs the received header
24
+
25
+ class Handler(http.server.BaseHTTPRequestHandler):
26
+ def log_message(self, *_): pass
27
+ def do_GET(self):
28
+ print("server:", self.headers.get('my-header'))
29
+ self.send_error(304)
30
+
31
+ server = http.server.HTTPServer(('localhost', 8080), Handler)
32
+ threading.Thread(target=server.handle_request).start()
33
+
34
+ # create a reader object
35
+
36
+ reader = make_reader(':memory:')
37
+ reader.add_feed('http://localhost:8080')
38
+
39
+ # set up a hook that adds the header to each request
40
+
41
+ def hook(session, request, **kwargs):
42
+ request.headers.setdefault('my-header', 'Hello, world!')
43
+
44
+ reader._parser.session_factory.request_hooks.append(hook)
45
+
46
+ # updating the feed sends the modified request to the server
47
+
48
+ print("updating...")
49
+ reader.update_feeds()
50
+ print("updated!")
@@ -11,7 +11,8 @@ retrieved asynchronously with `HTTPX <https://www.python-httpx.org/>`_:
11
11
  death and gravity
12
12
  Has your password been pwned? Or, how I almost failed to search a 37 GB text file in under 1 millisecond (in Python)
13
13
 
14
- """ # docstring-end
14
+ """
15
+
15
16
  import asyncio
16
17
  import io
17
18
  import httpx
@@ -12,6 +12,7 @@ To add feeds, run::
12
12
 
13
13
 
14
14
  """
15
+
15
16
  import logging
16
17
  import os
17
18
  import sys
@@ -26,6 +26,7 @@ classifiers = [
26
26
  "Programming Language :: Python :: 3.10",
27
27
  "Programming Language :: Python :: 3.11",
28
28
  "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
29
30
  "Programming Language :: Python :: Implementation :: CPython",
30
31
  "Programming Language :: Python :: Implementation :: PyPy",
31
32
  "Topic :: Internet",
@@ -102,19 +103,20 @@ tests = [
102
103
  "pytest>=4",
103
104
  "pytest-randomly",
104
105
  "pytest-subtests",
105
- "flaky",
106
+ "pytest-rerunfailures",
106
107
  "coverage",
107
108
  "pytest-cov",
108
109
  "requests-mock",
109
110
  # mechanicalsoup hard-depends on lxml (see below)
110
- 'mechanicalsoup; (implementation_name != "pypy" or python_version <= "3.9")',
111
+ 'mechanicalsoup; (implementation_name != "pypy" and python_version <= "3.12")',
111
112
  "requests-wsgi-adapter",
112
113
  # we want to test search with all known bs4 parsers.
113
- # lxml does not have a PyPy 3.10+ wheel.
114
- 'lxml; (implementation_name != "pypy" or python_version <= "3.9")',
114
+ # lxml usually does not have recent PyPy wheels.
115
+ # lxml usually does not have pre-relase CPython wheels.
116
+ 'lxml; (implementation_name != "pypy" and python_version <= "3.12")',
115
117
  "html5lib",
116
118
  # for bench.py
117
- 'numpy; (implementation_name != "pypy" and os_name == "posix")',
119
+ 'numpy; (implementation_name != "pypy" and os_name == "posix" and python_version <= "3.12")',
118
120
  # mypy does not work on pypy (yet).
119
121
  'mypy; implementation_name != "pypy"',
120
122
  "types-requests",
@@ -148,14 +150,15 @@ dev = [
148
150
  version = {attr = "reader.__version__"}
149
151
 
150
152
  [tool.pytest.ini_options]
153
+ addopts = "--strict-markers"
151
154
  markers = [
152
155
  "slow: mark a test as slow.",
153
156
  "requires_lxml: mark a test to only run in places where we have lxml.",
154
157
  "apptest: mark a test as a web app test (skipped sometimes).",
158
+ "noautoclose: skip 'autoclose' autouse fixture for a test."
155
159
  ]
156
- addopts = "--no-success-flaky-report"
157
160
  filterwarnings = [
158
- "ignore:No parser was explicitly specified::reader._html_utils",
161
+ "ignore:No parser was explicitly specified::reader._storage._html_utils",
159
162
  ]
160
163
  testpaths = ["tests"]
161
164
 
@@ -180,3 +183,14 @@ module = [
180
183
  "reader._vendor.feedparser.*",
181
184
  ]
182
185
  ignore_errors = true
186
+
187
+ [tool.isort]
188
+ profile = "black"
189
+ py_version = 310
190
+ src_paths = ["src", "tests"]
191
+ extend_skip = ["examples", "src/reader/_vendor"]
192
+ skip_gitignore = true
193
+ filter_files = true
194
+ force_single_line = true
195
+ order_by_type = false
196
+ lines_after_imports = 2