reader 3.12__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 (224) hide show
  1. {reader-3.12 → reader-3.13}/CHANGES.rst +52 -0
  2. {reader-3.12/src/reader.egg-info → reader-3.13}/PKG-INFO +2 -2
  3. {reader-3.12 → reader-3.13}/docs/api.rst +3 -0
  4. {reader-3.12 → reader-3.13}/docs/cli.rst +4 -2
  5. {reader-3.12 → reader-3.13}/docs/conf.py +3 -0
  6. {reader-3.12 → reader-3.13}/docs/dev.rst +13 -3
  7. {reader-3.12 → reader-3.13}/docs/guide.rst +114 -49
  8. {reader-3.12 → reader-3.13}/docs/install.rst +1 -3
  9. {reader-3.12 → reader-3.13}/docs/internal.rst +3 -0
  10. {reader-3.12 → reader-3.13}/examples/parser_only.py +1 -0
  11. {reader-3.12 → reader-3.13}/examples/terminal.py +1 -0
  12. {reader-3.12 → reader-3.13}/pyproject.toml +15 -3
  13. {reader-3.12 → reader-3.13}/scripts/bench.py +3 -2
  14. {reader-3.12 → reader-3.13}/scripts/debug_storage_stats.py +2 -1
  15. {reader-3.12 → reader-3.13}/scripts/generate_import_all.py +1 -0
  16. {reader-3.12 → reader-3.13}/scripts/jscontrols.py +3 -1
  17. {reader-3.12 → reader-3.13}/src/reader/__init__.py +3 -1
  18. {reader-3.12 → reader-3.13}/src/reader/__main__.py +1 -0
  19. {reader-3.12 → reader-3.13}/src/reader/_app/__init__.py +8 -5
  20. {reader-3.12 → reader-3.13}/src/reader/_app/api_thing.py +1 -0
  21. {reader-3.12 → reader-3.13}/src/reader/_app/cli.py +1 -0
  22. {reader-3.12 → reader-3.13}/src/reader/_app/wsgi.py +1 -0
  23. {reader-3.12 → reader-3.13}/src/reader/_cli.py +15 -5
  24. {reader-3.12 → reader-3.13}/src/reader/_config.py +3 -1
  25. {reader-3.12 → reader-3.13}/src/reader/_hash_utils.py +1 -0
  26. {reader-3.12 → reader-3.13}/src/reader/_parser/__init__.py +2 -7
  27. {reader-3.12 → reader-3.13}/src/reader/_parser/_http_utils.py +2 -0
  28. {reader-3.12 → reader-3.13}/src/reader/_parser/_lazy.py +5 -6
  29. {reader-3.12 → reader-3.13}/src/reader/_parser/_url_utils.py +1 -0
  30. {reader-3.12 → reader-3.13}/src/reader/_parser/file.py +1 -2
  31. {reader-3.12 → reader-3.13}/src/reader/_parser/http.py +1 -2
  32. {reader-3.12 → reader-3.13}/src/reader/_parser/requests/__init__.py +5 -3
  33. {reader-3.12 → reader-3.13}/src/reader/_plugins/cli_status.py +70 -46
  34. {reader-3.12 → reader-3.13}/src/reader/_plugins/enclosure_tags.py +2 -0
  35. {reader-3.12 → reader-3.13}/src/reader/_plugins/preview_feed_list.py +1 -0
  36. {reader-3.12 → reader-3.13}/src/reader/_plugins/share.py +1 -0
  37. {reader-3.12 → reader-3.13}/src/reader/_plugins/sqlite_releases.py +2 -0
  38. {reader-3.12 → reader-3.13}/src/reader/_plugins/timer.py +2 -1
  39. {reader-3.12 → reader-3.13}/src/reader/_storage/_base.py +6 -4
  40. {reader-3.12 → reader-3.13}/src/reader/_storage/_changes.py +1 -0
  41. {reader-3.12 → reader-3.13}/src/reader/_storage/_entries.py +31 -18
  42. {reader-3.12 → reader-3.13}/src/reader/_storage/_feeds.py +46 -87
  43. {reader-3.12 → reader-3.13}/src/reader/_storage/_html_utils.py +2 -0
  44. {reader-3.12 → reader-3.13}/src/reader/_storage/_schema.py +11 -2
  45. {reader-3.12 → reader-3.13}/src/reader/_storage/_search.py +32 -95
  46. {reader-3.12 → reader-3.13}/src/reader/_storage/_sql_utils.py +3 -0
  47. {reader-3.12 → reader-3.13}/src/reader/_storage/_sqlite_utils.py +3 -2
  48. {reader-3.12 → reader-3.13}/src/reader/_storage/_tags.py +1 -0
  49. {reader-3.12 → reader-3.13}/src/reader/_types.py +56 -35
  50. {reader-3.12 → reader-3.13}/src/reader/_update.py +193 -149
  51. {reader-3.12 → reader-3.13}/src/reader/core.py +64 -8
  52. {reader-3.12 → reader-3.13}/src/reader/exceptions.py +3 -2
  53. {reader-3.12 → reader-3.13}/src/reader/plugins/__init__.py +2 -0
  54. {reader-3.12 → reader-3.13}/src/reader/plugins/enclosure_dedupe.py +1 -0
  55. {reader-3.12 → reader-3.13}/src/reader/plugins/entry_dedupe.py +4 -1
  56. {reader-3.12 → reader-3.13}/src/reader/plugins/mark_as_read.py +29 -0
  57. {reader-3.12 → reader-3.13}/src/reader/plugins/readtime.py +2 -0
  58. {reader-3.12 → reader-3.13}/src/reader/plugins/ua_fallback.py +2 -0
  59. {reader-3.12 → reader-3.13}/src/reader/types.py +60 -22
  60. {reader-3.12 → reader-3.13/src/reader.egg-info}/PKG-INFO +2 -2
  61. {reader-3.12 → reader-3.13}/src/reader.egg-info/SOURCES.txt +1 -0
  62. {reader-3.12 → reader-3.13}/src/reader.egg-info/requires.txt +1 -1
  63. {reader-3.12 → reader-3.13}/tests/conftest.py +3 -19
  64. {reader-3.12 → reader-3.13}/tests/test_app.py +3 -2
  65. {reader-3.12 → reader-3.13}/tests/test_bench.py +2 -1
  66. {reader-3.12 → reader-3.13}/tests/test_cli.py +18 -2
  67. {reader-3.12 → reader-3.13}/tests/test_lazy_imports.py +2 -1
  68. {reader-3.12 → reader-3.13}/tests/test_parser.py +7 -4
  69. reader-3.13/tests/test_plugins_cli_status.py +105 -0
  70. {reader-3.12 → reader-3.13}/tests/test_plugins_enclosure_dedupe.py +1 -2
  71. {reader-3.12 → reader-3.13}/tests/test_plugins_mark_as_read.py +47 -0
  72. {reader-3.12 → reader-3.13}/tests/test_reader.py +432 -67
  73. {reader-3.12 → reader-3.13}/tests/test_reader_context.py +3 -3
  74. {reader-3.12 → reader-3.13}/tests/test_reader_deprecations.py +1 -0
  75. {reader-3.12 → reader-3.13}/tests/test_reader_filter.py +7 -4
  76. {reader-3.12 → reader-3.13}/tests/test_reader_integration.py +6 -3
  77. {reader-3.12 → reader-3.13}/tests/test_reader_sort.py +1 -0
  78. reader-3.13/tests/test_reader_update.py +163 -0
  79. {reader-3.12 → reader-3.13}/tests/test_search.py +3 -4
  80. {reader-3.12 → reader-3.13}/tests/test_storage.py +11 -14
  81. {reader-3.12 → reader-3.13}/tests/test_test_utils.py +5 -1
  82. {reader-3.12 → reader-3.13}/tests/test_utils.py +1 -0
  83. reader-3.12/tests/test_plugins_cli_status.py +0 -26
  84. {reader-3.12 → reader-3.13}/LICENSE +0 -0
  85. {reader-3.12 → reader-3.13}/MANIFEST.in +0 -0
  86. {reader-3.12 → reader-3.13}/README.rst +0 -0
  87. {reader-3.12 → reader-3.13}/docs/Makefile +0 -0
  88. {reader-3.12 → reader-3.13}/docs/app.rst +0 -0
  89. {reader-3.12 → reader-3.13}/docs/changelog.rst +0 -0
  90. {reader-3.12 → reader-3.13}/docs/config.rst +0 -0
  91. {reader-3.12 → reader-3.13}/docs/contributing.rst +0 -0
  92. {reader-3.12 → reader-3.13}/docs/dev-app.rst +0 -0
  93. {reader-3.12 → reader-3.13}/docs/images/redesign-01.png +0 -0
  94. {reader-3.12 → reader-3.13}/docs/index.rst +0 -0
  95. {reader-3.12 → reader-3.13}/docs/make.bat +0 -0
  96. {reader-3.12 → reader-3.13}/docs/plugins.rst +0 -0
  97. {reader-3.12 → reader-3.13}/docs/screenshots/dillo.png +0 -0
  98. {reader-3.12 → reader-3.13}/docs/screenshots/entries-feed.png +0 -0
  99. {reader-3.12 → reader-3.13}/docs/screenshots/entries.png +0 -0
  100. {reader-3.12 → reader-3.13}/docs/screenshots/entry-one.png +0 -0
  101. {reader-3.12 → reader-3.13}/docs/screenshots/entry-two.png +0 -0
  102. {reader-3.12 → reader-3.13}/docs/screenshots/feeds.png +0 -0
  103. {reader-3.12 → reader-3.13}/docs/screenshots/lynx.png +0 -0
  104. {reader-3.12 → reader-3.13}/docs/screenshots/search.png +0 -0
  105. {reader-3.12 → reader-3.13}/docs/tutorial.rst +0 -0
  106. {reader-3.12 → reader-3.13}/docs/why.rst +0 -0
  107. {reader-3.12 → reader-3.13}/examples/config.yaml +0 -0
  108. {reader-3.12 → reader-3.13}/examples/podcast.py +0 -0
  109. {reader-3.12 → reader-3.13}/run.sh +0 -0
  110. {reader-3.12 → reader-3.13}/scripts/.coverage +0 -0
  111. {reader-3.12 → reader-3.13}/scripts/backup.sh +0 -0
  112. {reader-3.12 → reader-3.13}/scripts/jscontrols.html +0 -0
  113. {reader-3.12 → reader-3.13}/scripts/lines.sh +0 -0
  114. {reader-3.12 → reader-3.13}/scripts/release.py +0 -0
  115. {reader-3.12 → reader-3.13}/setup.cfg +0 -0
  116. {reader-3.12 → reader-3.13}/src/reader/_app/static/controls.js +0 -0
  117. {reader-3.12 → reader-3.13}/src/reader/_app/static/style.css +0 -0
  118. {reader-3.12 → reader-3.13}/src/reader/_app/templates/add_entry.html +0 -0
  119. {reader-3.12 → reader-3.13}/src/reader/_app/templates/entries.html +0 -0
  120. {reader-3.12 → reader-3.13}/src/reader/_app/templates/entry.html +0 -0
  121. {reader-3.12 → reader-3.13}/src/reader/_app/templates/feeds.html +0 -0
  122. {reader-3.12 → reader-3.13}/src/reader/_app/templates/layout.html +0 -0
  123. {reader-3.12 → reader-3.13}/src/reader/_app/templates/macros.html +0 -0
  124. {reader-3.12 → reader-3.13}/src/reader/_app/templates/metadata.html +0 -0
  125. {reader-3.12 → reader-3.13}/src/reader/_app/templates/tags.html +0 -0
  126. {reader-3.12 → reader-3.13}/src/reader/_parser/feedparser.py +0 -0
  127. {reader-3.12 → reader-3.13}/src/reader/_parser/jsonfeed.py +1 -1
  128. {reader-3.12 → reader-3.13}/src/reader/_parser/requests/_lazy.py +2 -2
  129. {reader-3.12 → reader-3.13}/src/reader/_plugins/__init__.py +1 -1
  130. {reader-3.12 → reader-3.13}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  131. {reader-3.12 → reader-3.13}/src/reader/_storage/__init__.py +0 -0
  132. {reader-3.12 → reader-3.13}/src/reader/_utils.py +0 -0
  133. {reader-3.12 → reader-3.13}/src/reader/_vendor/__init__.py +0 -0
  134. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/__init__.py +0 -0
  135. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/api.py +0 -0
  136. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  137. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  138. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  139. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  140. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  141. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  142. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  143. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  144. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  145. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/encodings.py +0 -0
  146. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  147. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/html.py +0 -0
  148. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/http.py +0 -0
  149. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/mixin.py +0 -0
  150. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  151. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  152. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  153. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  154. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  155. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  156. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  157. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  158. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  159. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  160. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  161. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  162. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  163. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/py.typed +0 -0
  164. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  165. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/sgml.py +0 -0
  166. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/urls.py +0 -0
  167. {reader-3.12 → reader-3.13}/src/reader/_vendor/feedparser/util.py +0 -0
  168. {reader-3.12 → reader-3.13}/src/reader/py.typed +0 -0
  169. {reader-3.12 → reader-3.13}/src/reader.egg-info/dependency_links.txt +0 -0
  170. {reader-3.12 → reader-3.13}/src/reader.egg-info/top_level.txt +0 -0
  171. {reader-3.12 → reader-3.13}/tests/data/10.json +0 -0
  172. {reader-3.12 → reader-3.13}/tests/data/10.json.py +0 -0
  173. {reader-3.12 → reader-3.13}/tests/data/custom +0 -0
  174. {reader-3.12 → reader-3.13}/tests/data/empty.atom +0 -0
  175. {reader-3.12 → reader-3.13}/tests/data/empty.atom.py +0 -0
  176. {reader-3.12 → reader-3.13}/tests/data/empty.json +0 -0
  177. {reader-3.12 → reader-3.13}/tests/data/empty.json.py +0 -0
  178. {reader-3.12 → reader-3.13}/tests/data/empty.rss +0 -0
  179. {reader-3.12 → reader-3.13}/tests/data/empty.rss.py +0 -0
  180. {reader-3.12 → reader-3.13}/tests/data/full.atom +0 -0
  181. {reader-3.12 → reader-3.13}/tests/data/full.atom.py +0 -0
  182. {reader-3.12 → reader-3.13}/tests/data/full.json +0 -0
  183. {reader-3.12 → reader-3.13}/tests/data/full.json.py +0 -0
  184. {reader-3.12 → reader-3.13}/tests/data/full.rss +0 -0
  185. {reader-3.12 → reader-3.13}/tests/data/full.rss.py +0 -0
  186. {reader-3.12 → reader-3.13}/tests/data/invalid.json +0 -0
  187. {reader-3.12 → reader-3.13}/tests/data/invalid.json.py +0 -0
  188. {reader-3.12 → reader-3.13}/tests/data/relative.atom +0 -0
  189. {reader-3.12 → reader-3.13}/tests/data/relative.atom.py +0 -0
  190. {reader-3.12 → reader-3.13}/tests/data/relative.rss +0 -0
  191. {reader-3.12 → reader-3.13}/tests/data/relative.rss.py +0 -0
  192. {reader-3.12 → reader-3.13}/tests/data/sqlite_releases.html +0 -0
  193. {reader-3.12 → reader-3.13}/tests/data/unknown.json +0 -0
  194. {reader-3.12 → reader-3.13}/tests/data/unknown.json.py +0 -0
  195. {reader-3.12 → reader-3.13}/tests/fakeparser.py +0 -0
  196. {reader-3.12 → reader-3.13}/tests/reader_methods.py +0 -0
  197. {reader-3.12 → reader-3.13}/tests/reader_test_plugins/__init__.py +0 -0
  198. {reader-3.12 → reader-3.13}/tests/reader_test_plugins/good.py +0 -0
  199. {reader-3.12 → reader-3.13}/tests/reader_test_plugins/init_error.py +0 -0
  200. {reader-3.12 → reader-3.13}/tests/reader_test_plugins/missing_dependency.py +0 -0
  201. {reader-3.12 → reader-3.13}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  202. {reader-3.12 → reader-3.13}/tests/test__types.py +0 -0
  203. {reader-3.12 → reader-3.13}/tests/test_app_wsgi.py +1 -1
  204. {reader-3.12 → reader-3.13}/tests/test_changes.py +1 -1
  205. {reader-3.12 → reader-3.13}/tests/test_config.py +0 -0
  206. {reader-3.12 → reader-3.13}/tests/test_exceptions.py +0 -0
  207. {reader-3.12 → reader-3.13}/tests/test_hash_utils.py +0 -0
  208. {reader-3.12 → reader-3.13}/tests/test_html_utils.py +0 -0
  209. {reader-3.12 → reader-3.13}/tests/test_plugins_entry_dedupe.py +2 -2
  210. {reader-3.12 → reader-3.13}/tests/test_plugins_preview_feed_list.py +2 -2
  211. {reader-3.12 → reader-3.13}/tests/test_plugins_readtime.py +3 -3
  212. {reader-3.12 → reader-3.13}/tests/test_plugins_sqlite_releases.py +1 -1
  213. {reader-3.12 → reader-3.13}/tests/test_plugins_ua_fallback.py +0 -0
  214. {reader-3.12 → reader-3.13}/tests/test_reader_counts.py +3 -3
  215. {reader-3.12 → reader-3.13}/tests/test_reader_hooks.py +4 -4
  216. {reader-3.12 → reader-3.13}/tests/test_reader_plugins.py +0 -0
  217. {reader-3.12 → reader-3.13}/tests/test_reader_private.py +3 -3
  218. {reader-3.12 → reader-3.13}/tests/test_reader_search.py +5 -5
  219. {reader-3.12 → reader-3.13}/tests/test_sql_utils.py +0 -0
  220. {reader-3.12 → reader-3.13}/tests/test_sqlite_utils.py +1 -1
  221. {reader-3.12 → reader-3.13}/tests/test_tags.py +2 -2
  222. {reader-3.12 → reader-3.13}/tests/test_types.py +0 -0
  223. {reader-3.12 → reader-3.13}/tests/utils.py +0 -0
  224. {reader-3.12 → reader-3.13}/tox.ini +0 -0
@@ -6,6 +6,58 @@ 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
+
9
61
  Version 3.12
10
62
  ------------
11
63
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.12
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
 
@@ -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,18 @@ 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
718
724
 
719
- 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.
720
730
 
721
731
 
722
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,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
 
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Extract query timings from (stderr) log output of:
3
3
 
4
- reader --debug-storage [update | update --new-only | search update] --vv
4
+ reader --debug-storage [update | update --new | search update] --vv
5
5
 
6
6
  Note that the timings are not entirely accurate:
7
7
 
@@ -16,6 +16,7 @@ and how often they time out.
16
16
  Initially made for #175 and #178.
17
17
 
18
18
  """
19
+
19
20
  import json
20
21
  import re
21
22
  import sys
@@ -1,6 +1,7 @@
1
1
  import inspect
2
2
  import random
3
3
 
4
+
4
5
  context = {}
5
6
  exec('from reader import *', context)
6
7
  context.pop('__builtins__')
@@ -8,10 +8,12 @@ from flask import jsonify
8
8
  from flask import redirect
9
9
  from flask import request
10
10
 
11
+
11
12
  root_dir = os.path.dirname(__file__)
12
13
  sys.path.insert(0, os.path.join(root_dir, '../src'))
13
14
 
14
- from reader._app.api_thing import APIThing, APIError
15
+ from reader._app.api_thing import APIError
16
+ from reader._app.api_thing import APIThing
15
17
 
16
18
 
17
19
  app = Flask(
@@ -37,7 +37,9 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.12'
40
+ __version__ = '3.13'
41
+
42
+ # isort: off
41
43
 
42
44
  from .core import (
43
45
  Reader as Reader,
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
 
3
+
3
4
  CANNOT_IMPORT = """\
4
5
  Error: cannot import reader._cli
5
6
 
@@ -28,8 +28,6 @@ from flask import stream_with_context
28
28
  from flask import url_for
29
29
 
30
30
  import reader
31
- from .api_thing import APIError
32
- from .api_thing import APIThing
33
31
  from reader import Content
34
32
  from reader import Entry
35
33
  from reader import EntrySearchResult
@@ -40,6 +38,9 @@ from reader._plugins import Loader
40
38
  from reader.types import _get_entry_content
41
39
  from reader.types import TristateFilterInput
42
40
 
41
+ from .api_thing import APIError
42
+ from .api_thing import APIThing
43
+
43
44
 
44
45
  blueprint = Blueprint('reader', __name__)
45
46
 
@@ -491,9 +492,11 @@ def feeds():
491
492
  (
492
493
  feed,
493
494
  list(reader.get_tag_keys(feed)),
494
- reader.get_entry_counts(feed=feed)
495
- if with_counts or sort in FEED_SORT_FANCY
496
- else None,
495
+ (
496
+ reader.get_entry_counts(feed=feed)
497
+ if with_counts or sort in FEED_SORT_FANCY
498
+ else None
499
+ ),
497
500
  )
498
501
  for feed in feeds
499
502
  )
@@ -6,6 +6,7 @@ Contains no business logic.
6
6
  See scripts/jscontrols.py for a minimal usage example.
7
7
 
8
8
  """
9
+
9
10
  from urllib.parse import urljoin
10
11
  from urllib.parse import urlparse
11
12
 
@@ -30,6 +30,7 @@ def serve(config, host, port, plugin, verbose):
30
30
  """Start a local HTTP reader server."""
31
31
  setup_logging(verbose)
32
32
  from werkzeug.serving import run_simple
33
+
33
34
  from . import create_app
34
35
 
35
36
  if plugin:
@@ -8,6 +8,7 @@ To run a local development server:
8
8
  flask run -h 0.0.0.0 -p 8000
9
9
 
10
10
  """
11
+
11
12
  import os
12
13
 
13
14
  import yaml