reader 3.11__tar.gz → 3.13__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 (227) hide show
  1. {reader-3.11 → reader-3.13}/CHANGES.rst +74 -0
  2. {reader-3.11/src/reader.egg-info → reader-3.13}/PKG-INFO +2 -2
  3. {reader-3.11 → reader-3.13}/docs/api.rst +3 -0
  4. {reader-3.11 → reader-3.13}/docs/cli.rst +4 -2
  5. {reader-3.11 → reader-3.13}/docs/conf.py +3 -0
  6. {reader-3.11 → reader-3.13}/docs/dev.rst +20 -5
  7. {reader-3.11 → reader-3.13}/docs/guide.rst +137 -59
  8. {reader-3.11 → reader-3.13}/docs/install.rst +1 -3
  9. {reader-3.11 → reader-3.13}/docs/internal.rst +29 -3
  10. {reader-3.11 → reader-3.13}/examples/parser_only.py +1 -0
  11. {reader-3.11 → reader-3.13}/examples/terminal.py +1 -0
  12. {reader-3.11 → reader-3.13}/pyproject.toml +15 -3
  13. {reader-3.11 → reader-3.13}/scripts/bench.py +3 -2
  14. {reader-3.11 → reader-3.13}/scripts/debug_storage_stats.py +2 -1
  15. {reader-3.11 → reader-3.13}/scripts/generate_import_all.py +1 -0
  16. {reader-3.11 → reader-3.13}/scripts/jscontrols.py +3 -1
  17. {reader-3.11 → reader-3.13}/src/reader/__init__.py +3 -1
  18. {reader-3.11 → reader-3.13}/src/reader/__main__.py +1 -0
  19. {reader-3.11 → reader-3.13}/src/reader/_app/__init__.py +8 -5
  20. {reader-3.11 → reader-3.13}/src/reader/_app/api_thing.py +1 -0
  21. {reader-3.11 → reader-3.13}/src/reader/_app/cli.py +1 -3
  22. {reader-3.11 → reader-3.13}/src/reader/_app/wsgi.py +1 -0
  23. {reader-3.11 → reader-3.13}/src/reader/_cli.py +18 -35
  24. {reader-3.11 → reader-3.13}/src/reader/_config.py +3 -1
  25. {reader-3.11 → reader-3.13}/src/reader/_hash_utils.py +1 -0
  26. {reader-3.11 → reader-3.13}/src/reader/_parser/__init__.py +2 -7
  27. {reader-3.11 → reader-3.13}/src/reader/_parser/_http_utils.py +2 -0
  28. {reader-3.11 → reader-3.13}/src/reader/_parser/_lazy.py +5 -6
  29. {reader-3.11 → reader-3.13}/src/reader/_parser/_url_utils.py +1 -0
  30. {reader-3.11 → reader-3.13}/src/reader/_parser/file.py +1 -2
  31. {reader-3.11 → reader-3.13}/src/reader/_parser/http.py +1 -2
  32. {reader-3.11 → reader-3.13}/src/reader/_parser/requests/__init__.py +5 -3
  33. {reader-3.11 → reader-3.13}/src/reader/_plugins/cli_status.py +70 -46
  34. {reader-3.11 → reader-3.13}/src/reader/_plugins/enclosure_tags.py +2 -0
  35. {reader-3.11 → reader-3.13}/src/reader/_plugins/preview_feed_list.py +1 -0
  36. {reader-3.11 → reader-3.13}/src/reader/_plugins/share.py +1 -0
  37. {reader-3.11 → reader-3.13}/src/reader/_plugins/sqlite_releases.py +2 -0
  38. {reader-3.11 → reader-3.13}/src/reader/_plugins/timer.py +2 -1
  39. reader-3.13/src/reader/_storage/__init__.py +39 -0
  40. reader-3.13/src/reader/_storage/_base.py +112 -0
  41. reader-3.13/src/reader/_storage/_changes.py +214 -0
  42. reader-3.13/src/reader/_storage/_entries.py +667 -0
  43. reader-3.13/src/reader/_storage/_feeds.py +363 -0
  44. {reader-3.11/src/reader → reader-3.13/src/reader/_storage}/_html_utils.py +5 -1
  45. reader-3.13/src/reader/_storage/_schema.py +293 -0
  46. reader-3.13/src/reader/_storage/_search.py +580 -0
  47. {reader-3.11/src/reader → reader-3.13/src/reader/_storage}/_sql_utils.py +97 -26
  48. {reader-3.11/src/reader → reader-3.13/src/reader/_storage}/_sqlite_utils.py +125 -105
  49. reader-3.13/src/reader/_storage/_tags.py +251 -0
  50. {reader-3.11 → reader-3.13}/src/reader/_types.py +207 -34
  51. {reader-3.11 → reader-3.13}/src/reader/_update.py +193 -149
  52. {reader-3.11 → reader-3.13}/src/reader/_utils.py +0 -60
  53. {reader-3.11 → reader-3.13}/src/reader/core.py +65 -10
  54. {reader-3.11 → reader-3.13}/src/reader/exceptions.py +15 -2
  55. {reader-3.11 → reader-3.13}/src/reader/plugins/__init__.py +2 -0
  56. {reader-3.11 → reader-3.13}/src/reader/plugins/enclosure_dedupe.py +1 -0
  57. {reader-3.11 → reader-3.13}/src/reader/plugins/entry_dedupe.py +4 -1
  58. {reader-3.11 → reader-3.13}/src/reader/plugins/mark_as_read.py +29 -0
  59. {reader-3.11 → reader-3.13}/src/reader/plugins/readtime.py +4 -2
  60. {reader-3.11 → reader-3.13}/src/reader/plugins/ua_fallback.py +2 -0
  61. {reader-3.11 → reader-3.13}/src/reader/types.py +70 -22
  62. {reader-3.11 → reader-3.13/src/reader.egg-info}/PKG-INFO +2 -2
  63. {reader-3.11 → reader-3.13}/src/reader.egg-info/SOURCES.txt +13 -5
  64. {reader-3.11 → reader-3.13}/src/reader.egg-info/requires.txt +1 -1
  65. {reader-3.11 → reader-3.13}/tests/conftest.py +3 -21
  66. {reader-3.11 → reader-3.13}/tests/test_app.py +3 -2
  67. {reader-3.11 → reader-3.13}/tests/test_bench.py +2 -1
  68. reader-3.13/tests/test_changes.py +427 -0
  69. {reader-3.11 → reader-3.13}/tests/test_cli.py +18 -2
  70. {reader-3.11 → reader-3.13}/tests/test_html_utils.py +1 -1
  71. {reader-3.11 → reader-3.13}/tests/test_lazy_imports.py +2 -1
  72. {reader-3.11 → reader-3.13}/tests/test_parser.py +7 -4
  73. reader-3.13/tests/test_plugins_cli_status.py +105 -0
  74. {reader-3.11 → reader-3.13}/tests/test_plugins_enclosure_dedupe.py +1 -2
  75. {reader-3.11 → reader-3.13}/tests/test_plugins_mark_as_read.py +47 -0
  76. {reader-3.11 → reader-3.13}/tests/test_reader.py +433 -70
  77. {reader-3.11 → reader-3.13}/tests/test_reader_context.py +7 -5
  78. {reader-3.11 → reader-3.13}/tests/test_reader_deprecations.py +1 -0
  79. {reader-3.11 → reader-3.13}/tests/test_reader_filter.py +7 -4
  80. {reader-3.11 → reader-3.13}/tests/test_reader_integration.py +6 -3
  81. {reader-3.11 → reader-3.13}/tests/test_reader_search.py +117 -281
  82. {reader-3.11 → reader-3.13}/tests/test_reader_sort.py +1 -0
  83. reader-3.13/tests/test_reader_update.py +163 -0
  84. reader-3.13/tests/test_search.py +217 -0
  85. {reader-3.11 → reader-3.13}/tests/test_sql_utils.py +2 -2
  86. {reader-3.11 → reader-3.13}/tests/test_sqlite_utils.py +125 -90
  87. {reader-3.11 → reader-3.13}/tests/test_storage.py +52 -74
  88. {reader-3.11 → reader-3.13}/tests/test_test_utils.py +5 -1
  89. {reader-3.11 → reader-3.13}/tests/test_utils.py +1 -0
  90. {reader-3.11 → reader-3.13}/tox.ini +1 -1
  91. reader-3.11/src/reader/_search.py +0 -988
  92. reader-3.11/src/reader/_storage.py +0 -1808
  93. reader-3.11/tests/test_plugins_cli_status.py +0 -26
  94. reader-3.11/tests/test_search.py +0 -204
  95. {reader-3.11 → reader-3.13}/LICENSE +0 -0
  96. {reader-3.11 → reader-3.13}/MANIFEST.in +0 -0
  97. {reader-3.11 → reader-3.13}/README.rst +0 -0
  98. {reader-3.11 → reader-3.13}/docs/Makefile +0 -0
  99. {reader-3.11 → reader-3.13}/docs/app.rst +0 -0
  100. {reader-3.11 → reader-3.13}/docs/changelog.rst +0 -0
  101. {reader-3.11 → reader-3.13}/docs/config.rst +0 -0
  102. {reader-3.11 → reader-3.13}/docs/contributing.rst +0 -0
  103. {reader-3.11 → reader-3.13}/docs/dev-app.rst +0 -0
  104. {reader-3.11 → reader-3.13}/docs/images/redesign-01.png +0 -0
  105. {reader-3.11 → reader-3.13}/docs/index.rst +0 -0
  106. {reader-3.11 → reader-3.13}/docs/make.bat +0 -0
  107. {reader-3.11 → reader-3.13}/docs/plugins.rst +0 -0
  108. {reader-3.11 → reader-3.13}/docs/screenshots/dillo.png +0 -0
  109. {reader-3.11 → reader-3.13}/docs/screenshots/entries-feed.png +0 -0
  110. {reader-3.11 → reader-3.13}/docs/screenshots/entries.png +0 -0
  111. {reader-3.11 → reader-3.13}/docs/screenshots/entry-one.png +0 -0
  112. {reader-3.11 → reader-3.13}/docs/screenshots/entry-two.png +0 -0
  113. {reader-3.11 → reader-3.13}/docs/screenshots/feeds.png +0 -0
  114. {reader-3.11 → reader-3.13}/docs/screenshots/lynx.png +0 -0
  115. {reader-3.11 → reader-3.13}/docs/screenshots/search.png +0 -0
  116. {reader-3.11 → reader-3.13}/docs/tutorial.rst +0 -0
  117. {reader-3.11 → reader-3.13}/docs/why.rst +0 -0
  118. {reader-3.11 → reader-3.13}/examples/config.yaml +0 -0
  119. {reader-3.11 → reader-3.13}/examples/podcast.py +0 -0
  120. {reader-3.11 → reader-3.13}/run.sh +0 -0
  121. {reader-3.11 → reader-3.13}/scripts/.coverage +0 -0
  122. {reader-3.11 → reader-3.13}/scripts/backup.sh +0 -0
  123. {reader-3.11 → reader-3.13}/scripts/jscontrols.html +0 -0
  124. {reader-3.11 → reader-3.13}/scripts/lines.sh +0 -0
  125. {reader-3.11 → reader-3.13}/scripts/release.py +0 -0
  126. {reader-3.11 → reader-3.13}/setup.cfg +0 -0
  127. {reader-3.11 → reader-3.13}/src/reader/_app/static/controls.js +0 -0
  128. {reader-3.11 → reader-3.13}/src/reader/_app/static/style.css +0 -0
  129. {reader-3.11 → reader-3.13}/src/reader/_app/templates/add_entry.html +0 -0
  130. {reader-3.11 → reader-3.13}/src/reader/_app/templates/entries.html +0 -0
  131. {reader-3.11 → reader-3.13}/src/reader/_app/templates/entry.html +0 -0
  132. {reader-3.11 → reader-3.13}/src/reader/_app/templates/feeds.html +0 -0
  133. {reader-3.11 → reader-3.13}/src/reader/_app/templates/layout.html +0 -0
  134. {reader-3.11 → reader-3.13}/src/reader/_app/templates/macros.html +0 -0
  135. {reader-3.11 → reader-3.13}/src/reader/_app/templates/metadata.html +0 -0
  136. {reader-3.11 → reader-3.13}/src/reader/_app/templates/tags.html +0 -0
  137. {reader-3.11 → reader-3.13}/src/reader/_parser/feedparser.py +0 -0
  138. {reader-3.11 → reader-3.13}/src/reader/_parser/jsonfeed.py +1 -1
  139. {reader-3.11 → reader-3.13}/src/reader/_parser/requests/_lazy.py +2 -2
  140. {reader-3.11 → reader-3.13}/src/reader/_plugins/__init__.py +1 -1
  141. {reader-3.11 → reader-3.13}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  142. {reader-3.11 → reader-3.13}/src/reader/_vendor/__init__.py +0 -0
  143. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/__init__.py +0 -0
  144. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/api.py +0 -0
  145. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  146. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  147. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  148. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  149. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  150. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  151. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  152. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  153. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  154. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/encodings.py +0 -0
  155. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  156. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/html.py +0 -0
  157. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/http.py +0 -0
  158. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/mixin.py +0 -0
  159. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  160. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  161. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  162. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  163. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  164. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  165. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  166. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  167. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  168. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  169. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  170. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  171. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  172. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/py.typed +0 -0
  173. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  174. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/sgml.py +0 -0
  175. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/urls.py +0 -0
  176. {reader-3.11 → reader-3.13}/src/reader/_vendor/feedparser/util.py +0 -0
  177. {reader-3.11 → reader-3.13}/src/reader/py.typed +0 -0
  178. {reader-3.11 → reader-3.13}/src/reader.egg-info/dependency_links.txt +0 -0
  179. {reader-3.11 → reader-3.13}/src/reader.egg-info/top_level.txt +0 -0
  180. {reader-3.11 → reader-3.13}/tests/data/10.json +0 -0
  181. {reader-3.11 → reader-3.13}/tests/data/10.json.py +0 -0
  182. {reader-3.11 → reader-3.13}/tests/data/custom +0 -0
  183. {reader-3.11 → reader-3.13}/tests/data/empty.atom +0 -0
  184. {reader-3.11 → reader-3.13}/tests/data/empty.atom.py +0 -0
  185. {reader-3.11 → reader-3.13}/tests/data/empty.json +0 -0
  186. {reader-3.11 → reader-3.13}/tests/data/empty.json.py +0 -0
  187. {reader-3.11 → reader-3.13}/tests/data/empty.rss +0 -0
  188. {reader-3.11 → reader-3.13}/tests/data/empty.rss.py +0 -0
  189. {reader-3.11 → reader-3.13}/tests/data/full.atom +0 -0
  190. {reader-3.11 → reader-3.13}/tests/data/full.atom.py +0 -0
  191. {reader-3.11 → reader-3.13}/tests/data/full.json +0 -0
  192. {reader-3.11 → reader-3.13}/tests/data/full.json.py +0 -0
  193. {reader-3.11 → reader-3.13}/tests/data/full.rss +0 -0
  194. {reader-3.11 → reader-3.13}/tests/data/full.rss.py +0 -0
  195. {reader-3.11 → reader-3.13}/tests/data/invalid.json +0 -0
  196. {reader-3.11 → reader-3.13}/tests/data/invalid.json.py +0 -0
  197. {reader-3.11 → reader-3.13}/tests/data/relative.atom +0 -0
  198. {reader-3.11 → reader-3.13}/tests/data/relative.atom.py +0 -0
  199. {reader-3.11 → reader-3.13}/tests/data/relative.rss +0 -0
  200. {reader-3.11 → reader-3.13}/tests/data/relative.rss.py +0 -0
  201. {reader-3.11 → reader-3.13}/tests/data/sqlite_releases.html +0 -0
  202. {reader-3.11 → reader-3.13}/tests/data/unknown.json +0 -0
  203. {reader-3.11 → reader-3.13}/tests/data/unknown.json.py +0 -0
  204. {reader-3.11 → reader-3.13}/tests/fakeparser.py +0 -0
  205. {reader-3.11 → reader-3.13}/tests/reader_methods.py +0 -0
  206. {reader-3.11 → reader-3.13}/tests/reader_test_plugins/__init__.py +0 -0
  207. {reader-3.11 → reader-3.13}/tests/reader_test_plugins/good.py +0 -0
  208. {reader-3.11 → reader-3.13}/tests/reader_test_plugins/init_error.py +0 -0
  209. {reader-3.11 → reader-3.13}/tests/reader_test_plugins/missing_dependency.py +0 -0
  210. {reader-3.11 → reader-3.13}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  211. {reader-3.11 → reader-3.13}/tests/test__types.py +0 -0
  212. {reader-3.11 → reader-3.13}/tests/test_app_wsgi.py +1 -1
  213. {reader-3.11 → reader-3.13}/tests/test_config.py +0 -0
  214. {reader-3.11 → reader-3.13}/tests/test_exceptions.py +0 -0
  215. {reader-3.11 → reader-3.13}/tests/test_hash_utils.py +0 -0
  216. {reader-3.11 → reader-3.13}/tests/test_plugins_entry_dedupe.py +2 -2
  217. {reader-3.11 → reader-3.13}/tests/test_plugins_preview_feed_list.py +2 -2
  218. {reader-3.11 → reader-3.13}/tests/test_plugins_readtime.py +3 -3
  219. {reader-3.11 → reader-3.13}/tests/test_plugins_sqlite_releases.py +1 -1
  220. {reader-3.11 → reader-3.13}/tests/test_plugins_ua_fallback.py +0 -0
  221. {reader-3.11 → reader-3.13}/tests/test_reader_counts.py +3 -3
  222. {reader-3.11 → reader-3.13}/tests/test_reader_hooks.py +4 -4
  223. {reader-3.11 → reader-3.13}/tests/test_reader_plugins.py +0 -0
  224. {reader-3.11 → reader-3.13}/tests/test_reader_private.py +3 -3
  225. {reader-3.11 → reader-3.13}/tests/test_tags.py +2 -2
  226. {reader-3.11 → reader-3.13}/tests/test_types.py +0 -0
  227. {reader-3.11 → reader-3.13}/tests/utils.py +0 -0
@@ -6,6 +6,80 @@ Changelog
6
6
  :noindex:
7
7
 
8
8
 
9
+ Version 3.13
10
+ ------------
11
+
12
+ Released 2024-06-19
13
+
14
+ * Allow different feed update intervals; see :ref:`scheduled` for details.
15
+ (:issue:`332`)
16
+
17
+ * Add ``scheduled`` argument to :meth:`~Reader.update_feeds()`,
18
+ :meth:`~Reader.get_feeds()`, etc.
19
+
20
+ * Add :attr:`Feed.update_after` and :attr:`~Feed.last_retrieved` attributes.
21
+
22
+ * The ``new`` filter of :meth:`~Reader.update_feeds()` etc. considers
23
+ a feed new if it was never retrieved (:attr:`~Feed.last_retrieved`),
24
+ not if it was never updated successfully (:attr:`~Feed.last_updated`).
25
+
26
+ * Update an entry whenever its :attr:`~Entry.updated` changes
27
+ (don't require it to be newer).
28
+
29
+ * Fix bug introduced in `version 3.12 <Version 3.12_>`_ causing an assertion error
30
+ when there are multiple entries with the same id in the same feed,
31
+ or when parallel :meth:`~Reader.update_feeds` calls add the same entry.
32
+ The fix restores the pre-3.12 first-entry-wins / last-write-wins behavior.
33
+ Thanks to `Joakim Hellsén`_ for reporting and helping debug this issue.
34
+ (:issue:`335`)
35
+
36
+ * Fix assertion error when an entry is deleted while being updated.
37
+
38
+ * Allow re-running the :mod:`~reader.plugins.mark_as_read` plugin for existing entries.
39
+ Thanks to `Michael Han`_ for the pull request.
40
+ (:issue:`317`)
41
+
42
+ * Other changes for scheduled updates. (:issue:`332`)
43
+
44
+ * Add ``--scheduled`` flag to the ``update`` command.
45
+ * The :mod:`~reader._plugins.cli_status` plugin now
46
+ records the output of multiple runs instead of just the last one,
47
+ with output from the same hour grouped in a single entry,
48
+ up to 24 entries/hours.
49
+ * Group mutually-exclusive attributes of :class:`~.FeedUpdateIntent`
50
+ into a new :attr:`~.FeedUpdateIntent.value` union attribute.
51
+
52
+ * New and improved :ref:`update` user guide section.
53
+ * Rename ``update --new-only`` CLI flag to ``--new``;
54
+ ``--new-only`` remains available as an alias.
55
+ (:issue:`334`)
56
+
57
+ .. _Joakim Hellsén: https://github.com/TheLovinator1
58
+ .. _Michael Han: https://github.com/Sakunam
59
+
60
+
61
+ Version 3.12
62
+ ------------
63
+
64
+ Released 2024-03-05
65
+
66
+ * Split the :ref:`full-text search <fts>` index into a separate,
67
+ attached database.
68
+ (:issue:`323`)
69
+ * Require at least SQLite 3.18.
70
+ Previously, *reader* core required 3.15,
71
+ and only :meth:`~Reader.update_search` required 3.18.
72
+ (:issue:`323`)
73
+ * Enable `write-ahead logging`_ only once, when the database is created,
74
+ instead of every time it is opened.
75
+ (:issue:`323`)
76
+ * Vacuum the main database after migrations. (:issue:`323`)
77
+ * Add an internal :ref:`change tracking API <changes>`
78
+ to formalize how search keeps in sync with storage.
79
+ (:issue:`323`)
80
+ * Refactor storage internals. (:issue:`323`)
81
+
82
+
9
83
  Version 3.11
10
84
  ------------
11
85
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.11
3
+ Version: 3.13
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -61,7 +61,7 @@ Provides-Extra: tests
61
61
  Requires-Dist: pytest>=4; extra == "tests"
62
62
  Requires-Dist: pytest-randomly; extra == "tests"
63
63
  Requires-Dist: pytest-subtests; extra == "tests"
64
- Requires-Dist: flaky; extra == "tests"
64
+ Requires-Dist: pytest-rerunfailures; extra == "tests"
65
65
  Requires-Dist: coverage; extra == "tests"
66
66
  Requires-Dist: pytest-cov; extra == "tests"
67
67
  Requires-Dist: requests-mock; 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
 
@@ -57,7 +57,6 @@ but I will prioritize supporting :doc:`contributors <contributing>`
57
57
 
58
58
  * :ref:`searchable tag values <searchable tags>`, e.g. for comments
59
59
  * :ref:`unification with entry.read/important <entry flag unification>`
60
- * filter entries by entry tags, :issue:`328`
61
60
  * optimistic locking, :issue:`308`
62
61
  * filter tags by prefix, :issue:`309`
63
62
 
@@ -345,6 +344,8 @@ Storage internal API documented in version 3.10 (November 2023) in :issue:`325`.
345
344
  Database optimization
346
345
  ~~~~~~~~~~~~~~~~~~~~~
347
346
 
347
+ Optimization lessons learned while fixing "database is locked" errors: :issue:`175#issuecomment-657495233`.
348
+
348
349
  Some general guidance on schema/index design: :issue:`327#issuecomment-1859147186`.
349
350
 
350
351
  Speeding up ``get_entries(sort='recent')``:
@@ -449,7 +450,8 @@ https://death.andgravity.com/query-builder
449
450
  Pagination for methods that return iterators
450
451
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
451
452
 
452
- Why do it for the private implementation: :issue:`167#issuecomment-626753299` (also a comment in storage code).
453
+ Why do it for the private implementation: :issue:`167#issuecomment-626753299`;
454
+
453
455
 
454
456
  Detailed requirements and API discussion for public pagination: :issue:`196#issuecomment-706038363`.
455
457
 
@@ -464,6 +466,9 @@ From the initial issue:
464
466
 
465
467
  Enabling search by default, and alternative search APIs: :issue:`252`.
466
468
 
469
+ Change tracking API: :issue:`323#issuecomment-1930756417`, model validated in
470
+ `this gist <https://gist.github.com/lemon24/558955ad82ba2e4f50c0184c630c668c>`_.
471
+
467
472
  External resources:
468
473
 
469
474
  * Comprehensive, although a bit old (2017): `What every software engineer should know about search <https://medium.com/startup-grind/what-every-software-engineer-should-know-about-search-27d1df99f80d>`_ (`full version <http://webcache.googleusercontent.com/search?q=cache:https://medium.com/startup-grind/what-every-software-engineer-should-know-about-search-27d1df99f80d&sca_esv=570067020&prmd=ivn&strip=1&vwsrc=0>`_)
@@ -595,6 +600,8 @@ Updating entries based on a hash of their content (regardless of ``updated``):
595
600
 
596
601
  Decision to ignore feed.updated when updating feeds: :issue:`231`.
597
602
 
603
+ :ref:`scheduled` (initial design + postponed features): :issue:`322`.
604
+
598
605
 
599
606
  .. _deleting entries:
600
607
 
@@ -708,10 +715,18 @@ OPML support
708
715
  Thoughts on dynamic lists of feeds: :issue:`165#issuecomment-905893420`.
709
716
 
710
717
 
711
- entry_dedupe
712
- ~~~~~~~~~~~~
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
713
724
 
714
- Using MinHash to speed up similarity checks (maybe): https://gist.github.com/lemon24/b9af5ade919713406bda9603847d32e5
725
+ However, it is also possible for a feed to have two entries with the same id
726
+ – yes, even though in most (if not all) formats,
727
+ the id is meant to be *universally unique*!
728
+ As of 3.13, we do not support multiple entries with the same id
729
+ (the first entry wins); see :issue:`335` for thoughts on this.
715
730
 
716
731
 
717
732
  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,12 @@ 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
+
140
+ .. _backups:
141
+
130
142
  Back-ups
131
143
  ~~~~~~~~
132
144
 
@@ -205,42 +217,96 @@ use :meth:`~Reader.delete_feed`::
205
217
  Updating feeds
206
218
  --------------
207
219
 
208
- To retrieve the latest version of a feed, along with any new entries,
209
- it must be updated.
210
220
  You can update all the feeds by using the :meth:`~Reader.update_feeds` method::
211
221
 
212
222
  >>> reader.update_feeds()
213
223
  >>> reader.get_feed(feed)
214
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', ...)
215
225
 
216
-
217
226
  To retrive feeds in parallel, use the ``workers`` flag::
218
227
 
219
228
  >>> reader.update_feeds(workers=10)
220
229
 
221
-
222
- You can also update a specific feed using :meth:`~Reader.update_feed`::
230
+ You can update a single feed using :meth:`~Reader.update_feed`::
223
231
 
224
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
+
266
+
267
+ .. _scheduled:
268
+
269
+ Scheduled updates
270
+ ~~~~~~~~~~~~~~~~~
225
271
 
226
- If supported by the server, *reader* uses the ETag and Last-Modified headers
227
- to only retrieve feeds if they changed
228
- (`details <https://feedparser.readthedocs.io/en/latest/http-etag.html>`_).
229
- Even so, you should not update feeds *too* often,
230
- to avoid wasting the feed publisher's resources,
231
- and potentially getting banned;
232
- every 30 minutes seems reasonable.
272
+ Because different feeds need to be updated at different rates,
273
+ *reader* also provides a mechanism for scheduling updates.
233
274
 
234
- To support updating newly-added feeds off the regular update schedule,
235
- you can use the ``new_only`` flag;
236
- you can call this more often (e.g. every minute)::
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.
237
279
 
238
- >>> reader.update_feeds(new_only=True)
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.
239
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
+ ~~~~~~~~~~~~~
240
306
 
241
307
  If you need the status of each feed as it gets updated
242
308
  (for instance, to update a progress bar),
243
- you can use :meth:`~Reader.update_feeds_iter` instead,
309
+ you can use :meth:`~Reader.update_feeds_iter` instead of :meth:`~Reader.update_feeds`,
244
310
  and get a (url, updated feed or none or exception) pair for each feed::
245
311
 
246
312
  >>> for url, value in reader.update_feeds_iter():
@@ -255,9 +321,14 @@ and get a (url, updated feed or none or exception) pair for each feed::
255
321
  https://www.relay.fm/cortex/feed not modified
256
322
 
257
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`).
328
+
258
329
 
259
- Disabling feed updates
260
- ----------------------
330
+ Disabling updates
331
+ ~~~~~~~~~~~~~~~~~
261
332
 
262
333
  Sometimes, it is useful to skip a feed when using :meth:`~Reader.update_feeds`;
263
334
  for example, the feed does not exist anymore,
@@ -280,30 +351,22 @@ Getting feeds
280
351
  -------------
281
352
 
282
353
  As seen in the previous sections,
283
- :meth:`~Reader.get_feed` returns a :class:`Feed` object
284
- with more information about a feed::
354
+ :meth:`~Reader.get_feed` returns a :class:`Feed` object::
285
355
 
286
- >>> from prettyprinter import pprint, install_extras;
287
- >>> install_extras(include=['dataclasses'])
288
356
  >>> feed = reader.get_feed(feed)
289
357
  >>> pprint(feed)
290
- reader.types.Feed(
291
- url='http://www.hellointernet.fm/podcast?format=rss',
292
- updated=datetime.datetime(
293
- year=2020,
294
- month=2,
295
- day=28,
296
- hour=9,
297
- minute=34,
298
- second=2,
299
- tzinfo=datetime.timezone.utc
300
- ),
358
+ Feed(url='http://www.hellointernet.fm/podcast?format=rss',
359
+ updated=datetime.datetime(2020, 2, 28, 9, 34, 2, tzinfo=datetime.timezone.utc),
301
360
  title='Hello Internet',
302
361
  link='http://www.hellointernet.fm/',
303
362
  author='CGP Grey',
363
+ subtitle='CGP Grey and Brady Haran talk about YouTube, life, work, whatever.',
364
+ version='rss20',
365
+ user_title=None,
304
366
  added=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc),
305
- last_updated=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc)
306
- )
367
+ last_updated=datetime.datetime(2020, 10, 12, tzinfo=datetime.timezone.utc),
368
+ last_exception=None,
369
+ updates_enabled=True)
307
370
 
308
371
  To get all the feeds, use the :meth:`~Reader.get_feeds` method::
309
372
 
@@ -353,7 +416,7 @@ You can get all the entries, most-recent first,
353
416
  by using :meth:`~Reader.get_entries()`,
354
417
  which generates :class:`Entry` objects::
355
418
 
356
- >>> for entry, _ in zip(reader.get_entries(), range(10)):
419
+ >>> for entry in reader.get_entries(limit=10):
357
420
  ... print(entry.feed.title, '-', entry.title)
358
421
  ...
359
422
  Cortex - 106: Clear and Boring
@@ -368,12 +431,11 @@ Here is an example of getting entries for a single feed::
368
431
 
369
432
  >>> feed.title
370
433
  'Hello Internet'
371
- >>> entries = list(reader.get_entries(feed=feed))
372
- >>> for entry in entries[:2]:
373
- ... print(entry.feed.title, '-', entry.title)
434
+ >>> for entry in reader.get_entries(feed=feed, limit=2):
435
+ ... print(entry.title)
374
436
  ...
375
- Hello Internet - H.I. #136: Dog Bingo
376
- Hello Internet - H.I. #135: Place Your Bets
437
+ H.I. #136: Dog Bingo
438
+ H.I. #135: Place Your Bets
377
439
 
378
440
 
379
441
 
@@ -383,9 +445,11 @@ Entry flags
383
445
  Entries can be marked as :attr:`~Entry.read` or :attr:`~Entry.important`.
384
446
  These flags can be used for filtering::
385
447
 
386
- >>> reader.mark_entry_as_read(entries[0])
387
- >>> entries = list(reader.get_entries(feed=feed, read=False))
388
- >>> 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):
389
453
  ... print(entry.title)
390
454
  ...
391
455
  H.I. #135: Place Your Bets
@@ -439,16 +503,27 @@ the entries that changed since the last call,
439
503
  so it is OK to call it relatively often.
440
504
 
441
505
 
442
- Because search adds minor overhead to other :class:`Reader` methods
443
- and can almost double the size of the database,
444
- it can be turned on/off through the
445
- :meth:`~Reader.enable_search()` / :meth:`~Reader.disable_search()` methods.
446
- This is persistent across instances using the same database,
447
- and only needs to be done once.
448
- You can also use the ``search_enabled`` :func:`make_reader` argument
449
- for the same purpose.
450
- By default, search is disabled,
451
- and enabled automatically on the first :meth:`~Reader.update_search()` call.
506
+ Search can be turned on/off through the
507
+ :meth:`~Reader.enable_search()` / :meth:`~Reader.disable_search()` methods
508
+ (persistent across instances using the same database),
509
+ or the ``search_enabled`` argument of :func:`make_reader`;
510
+ by default, search is enabled automatically
511
+ on the first :meth:`~Reader.update_search()` call.
512
+ If search is enabled,
513
+ you should call :meth:`~Reader.update_search()` regularly
514
+ to prevent unprocesses changes from accumulating over time.
515
+
516
+
517
+ Because the search index can be almost as large as the main database,
518
+ the default implementation splits it into a separate, attached database,
519
+ which allows :ref:`backing up <backups>` the main database separately;
520
+ for a reader created with ``make_reader('db.sqlite')``,
521
+ the search index will be in ``db.sqlite.search``.
522
+
523
+
524
+ .. versionchanged:: 3.12
525
+ Split the full-text search index into a separate database.
526
+
452
527
 
453
528
 
454
529
 
@@ -753,7 +828,8 @@ This applies to the following names:
753
828
  * tag keys
754
829
  * the top-level keys of dict tag values
755
830
 
756
- 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`;
757
833
  new ones will be documented here.
758
834
 
759
835
  The prefixes can be changed using
@@ -881,6 +957,8 @@ depending on their type attribute or feedparser defaults:
881
957
 
882
958
 
883
959
 
960
+ .. _errors:
961
+
884
962
  Errors and exceptions
885
963
  ---------------------
886
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:
@@ -143,9 +143,11 @@ but at least one other implementation needs to exists before that.
143
143
 
144
144
  Currently, search is tightly-bound to a storage implementation
145
145
  (see :meth:`~.BoundSearchStorageType.make_search`).
146
- In the future, it may be possible to mix and match
147
- main storage and search implementations,
148
- but :class:`StorageType` will likely need
146
+ While the :ref:`change tracking API <changes>` allows
147
+ search implementations to keep in sync with text content changes,
148
+ there is no convenient way for :meth:`SearchType.search_entries`
149
+ to filter/sort results without storage cooperation;
150
+ :class:`StorageType` will need
149
151
  additional capabilities to support this.
150
152
 
151
153
 
@@ -167,6 +169,27 @@ but at least one other implementation needs to exists before that.
167
169
  :members:
168
170
 
169
171
 
172
+ .. _changes:
173
+
174
+ .. autoclass:: ChangeTrackingStorageType()
175
+ :members:
176
+ :show-inheritance:
177
+
178
+ .. autoclass:: ChangeTrackerType()
179
+ :members:
180
+
181
+ .. autoclass:: Change
182
+ :members:
183
+
184
+ .. autoclass:: Action
185
+ :members:
186
+
187
+ .. autoattribute:: reader.Entry._sequence
188
+
189
+ .. autoexception:: reader.exceptions.ChangeTrackingNotEnabledError
190
+ :show-inheritance:
191
+
192
+
170
193
  Data objects
171
194
  ~~~~~~~~~~~~
172
195
 
@@ -179,6 +202,9 @@ Data objects
179
202
  .. autoclass:: FeedUpdateIntent
180
203
  :members:
181
204
 
205
+ .. autoclass:: FeedToUpdate
206
+ :members:
207
+
182
208
  .. autoclass:: EntryUpdateIntent
183
209
  :members:
184
210
 
@@ -12,6 +12,7 @@ retrieved asynchronously with `HTTPX <https://www.python-httpx.org/>`_:
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
14
  """ # docstring-end
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
@@ -102,7 +102,7 @@ tests = [
102
102
  "pytest>=4",
103
103
  "pytest-randomly",
104
104
  "pytest-subtests",
105
- "flaky",
105
+ "pytest-rerunfailures",
106
106
  "coverage",
107
107
  "pytest-cov",
108
108
  "requests-mock",
@@ -148,14 +148,15 @@ dev = [
148
148
  version = {attr = "reader.__version__"}
149
149
 
150
150
  [tool.pytest.ini_options]
151
+ addopts = "--strict-markers"
151
152
  markers = [
152
153
  "slow: mark a test as slow.",
153
154
  "requires_lxml: mark a test to only run in places where we have lxml.",
154
155
  "apptest: mark a test as a web app test (skipped sometimes).",
156
+ "noautoclose: skip 'autoclose' autouse fixture for a test."
155
157
  ]
156
- addopts = "--no-success-flaky-report"
157
158
  filterwarnings = [
158
- "ignore:No parser was explicitly specified::reader._html_utils",
159
+ "ignore:No parser was explicitly specified::reader._storage._html_utils",
159
160
  ]
160
161
  testpaths = ["tests"]
161
162
 
@@ -180,3 +181,14 @@ module = [
180
181
  "reader._vendor.feedparser.*",
181
182
  ]
182
183
  ignore_errors = true
184
+
185
+ [tool.isort]
186
+ profile = "black"
187
+ py_version = 310
188
+ src_paths = ["src", "tests"]
189
+ extend_skip = ["examples", "src/reader/_vendor"]
190
+ skip_gitignore = true
191
+ filter_files = true
192
+ force_single_line = true
193
+ order_by_type = false
194
+ lines_after_imports = 2
@@ -14,14 +14,15 @@ from functools import partial
14
14
 
15
15
  import click
16
16
 
17
+
17
18
  root_dir = os.path.dirname(__file__)
18
19
  sys.path.insert(0, os.path.join(root_dir, '../src'))
19
20
  sys.path.insert(0, os.path.join(root_dir, '../tests'))
20
21
 
21
22
  from fakeparser import Parser
22
-
23
23
  from reader import make_reader
24
- from reader._app import create_app, get_reader
24
+ from reader._app import create_app
25
+ from reader._app import get_reader
25
26
  from reader._config import make_reader_config
26
27
 
27
28