reader 3.14__tar.gz → 3.15__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 (226) hide show
  1. {reader-3.14 → reader-3.15}/CHANGES.rst +38 -1
  2. {reader-3.14/src/reader.egg-info → reader-3.15}/PKG-INFO +1 -1
  3. {reader-3.14 → reader-3.15}/docs/api.rst +3 -1
  4. {reader-3.14 → reader-3.15}/docs/dev.rst +19 -9
  5. {reader-3.14 → reader-3.15}/docs/guide.rst +28 -20
  6. {reader-3.14 → reader-3.15}/docs/install.rst +14 -0
  7. {reader-3.14 → reader-3.15}/docs/internal.rst +34 -18
  8. {reader-3.14 → reader-3.15}/docs/plugins.rst +18 -1
  9. {reader-3.14 → reader-3.15}/docs/tutorial.rst +1 -1
  10. reader-3.15/examples/feed_slugs.py +95 -0
  11. {reader-3.14 → reader-3.15}/scripts/backup.sh +9 -6
  12. {reader-3.14 → reader-3.15}/src/reader/__init__.py +1 -1
  13. {reader-3.14 → reader-3.15}/src/reader/_cli.py +13 -20
  14. {reader-3.14 → reader-3.15}/src/reader/_parser/__init__.py +187 -83
  15. {reader-3.14 → reader-3.15}/src/reader/_parser/_http_utils.py +1 -0
  16. {reader-3.14 → reader-3.15}/src/reader/_parser/_lazy.py +166 -137
  17. {reader-3.14 → reader-3.15}/src/reader/_parser/feedparser.py +9 -3
  18. {reader-3.14 → reader-3.15}/src/reader/_parser/file.py +2 -6
  19. reader-3.15/src/reader/_parser/http.py +107 -0
  20. {reader-3.14 → reader-3.15}/src/reader/_parser/jsonfeed.py +1 -1
  21. {reader-3.14 → reader-3.15}/src/reader/_parser/requests/__init__.py +2 -0
  22. {reader-3.14 → reader-3.15}/src/reader/_parser/requests/_lazy.py +26 -6
  23. {reader-3.14 → reader-3.15}/src/reader/_storage/_base.py +7 -2
  24. {reader-3.14 → reader-3.15}/src/reader/_storage/_feeds.py +10 -9
  25. {reader-3.14 → reader-3.15}/src/reader/_storage/_schema.py +50 -3
  26. {reader-3.14 → reader-3.15}/src/reader/_storage/_search.py +12 -0
  27. {reader-3.14 → reader-3.15}/src/reader/_storage/_tags.py +158 -52
  28. {reader-3.14 → reader-3.15}/src/reader/_types.py +52 -84
  29. {reader-3.14 → reader-3.15}/src/reader/_update.py +56 -54
  30. {reader-3.14 → reader-3.15}/src/reader/_utils.py +9 -3
  31. {reader-3.14 → reader-3.15}/src/reader/core.py +20 -32
  32. {reader-3.14 → reader-3.15}/src/reader/exceptions.py +6 -10
  33. {reader-3.14 → reader-3.15}/src/reader/plugins/__init__.py +1 -1
  34. {reader-3.14 → reader-3.15}/src/reader/plugins/entry_dedupe.py +1 -1
  35. {reader-3.14 → reader-3.15}/src/reader/plugins/mark_as_read.py +1 -1
  36. {reader-3.14 → reader-3.15}/src/reader/plugins/readtime.py +1 -1
  37. {reader-3.14 → reader-3.15}/src/reader/types.py +2 -4
  38. {reader-3.14 → reader-3.15/src/reader.egg-info}/PKG-INFO +1 -1
  39. {reader-3.14 → reader-3.15}/src/reader.egg-info/SOURCES.txt +1 -0
  40. {reader-3.14 → reader-3.15}/tests/fakeparser.py +15 -16
  41. {reader-3.14 → reader-3.15}/tests/test_bench.py +1 -0
  42. {reader-3.14 → reader-3.15}/tests/test_cli.py +14 -0
  43. {reader-3.14 → reader-3.15}/tests/test_lazy_imports.py +11 -1
  44. {reader-3.14 → reader-3.15}/tests/test_parser.py +220 -89
  45. {reader-3.14 → reader-3.15}/tests/test_plugins_sqlite_releases.py +4 -2
  46. {reader-3.14 → reader-3.15}/tests/test_reader.py +43 -7
  47. {reader-3.14 → reader-3.15}/tests/test_reader_private.py +26 -20
  48. reader-3.14/src/reader/_parser/http.py +0 -108
  49. {reader-3.14 → reader-3.15}/LICENSE +0 -0
  50. {reader-3.14 → reader-3.15}/MANIFEST.in +0 -0
  51. {reader-3.14 → reader-3.15}/README.rst +0 -0
  52. {reader-3.14 → reader-3.15}/docs/Makefile +0 -0
  53. {reader-3.14 → reader-3.15}/docs/app.rst +0 -0
  54. {reader-3.14 → reader-3.15}/docs/changelog.rst +0 -0
  55. {reader-3.14 → reader-3.15}/docs/cli.rst +0 -0
  56. {reader-3.14 → reader-3.15}/docs/conf.py +0 -0
  57. {reader-3.14 → reader-3.15}/docs/config.rst +0 -0
  58. {reader-3.14 → reader-3.15}/docs/contributing.rst +0 -0
  59. {reader-3.14 → reader-3.15}/docs/dev-app.rst +0 -0
  60. {reader-3.14 → reader-3.15}/docs/images/redesign-01.png +0 -0
  61. {reader-3.14 → reader-3.15}/docs/index.rst +0 -0
  62. {reader-3.14 → reader-3.15}/docs/make.bat +0 -0
  63. {reader-3.14 → reader-3.15}/docs/screenshots/dillo.png +0 -0
  64. {reader-3.14 → reader-3.15}/docs/screenshots/entries-feed.png +0 -0
  65. {reader-3.14 → reader-3.15}/docs/screenshots/entries.png +0 -0
  66. {reader-3.14 → reader-3.15}/docs/screenshots/entry-one.png +0 -0
  67. {reader-3.14 → reader-3.15}/docs/screenshots/entry-two.png +0 -0
  68. {reader-3.14 → reader-3.15}/docs/screenshots/feeds.png +0 -0
  69. {reader-3.14 → reader-3.15}/docs/screenshots/lynx.png +0 -0
  70. {reader-3.14 → reader-3.15}/docs/screenshots/search.png +0 -0
  71. {reader-3.14 → reader-3.15}/docs/why.rst +0 -0
  72. {reader-3.14 → reader-3.15}/examples/config.yaml +0 -0
  73. {reader-3.14 → reader-3.15}/examples/custom_headers.py +0 -0
  74. {reader-3.14 → reader-3.15}/examples/parser_only.py +0 -0
  75. {reader-3.14 → reader-3.15}/examples/podcast.py +0 -0
  76. {reader-3.14 → reader-3.15}/examples/terminal.py +0 -0
  77. {reader-3.14 → reader-3.15}/pyproject.toml +0 -0
  78. {reader-3.14 → reader-3.15}/run.sh +0 -0
  79. {reader-3.14 → reader-3.15}/scripts/.coverage +0 -0
  80. {reader-3.14 → reader-3.15}/scripts/bench.py +0 -0
  81. {reader-3.14 → reader-3.15}/scripts/debug_storage_stats.py +0 -0
  82. {reader-3.14 → reader-3.15}/scripts/generate_import_all.py +0 -0
  83. {reader-3.14 → reader-3.15}/scripts/jscontrols.html +0 -0
  84. {reader-3.14 → reader-3.15}/scripts/jscontrols.py +0 -0
  85. {reader-3.14 → reader-3.15}/scripts/lines.sh +0 -0
  86. {reader-3.14 → reader-3.15}/scripts/release.py +0 -0
  87. {reader-3.14 → reader-3.15}/setup.cfg +0 -0
  88. {reader-3.14 → reader-3.15}/src/reader/__main__.py +0 -0
  89. {reader-3.14 → reader-3.15}/src/reader/_app/__init__.py +0 -0
  90. {reader-3.14 → reader-3.15}/src/reader/_app/api_thing.py +0 -0
  91. {reader-3.14 → reader-3.15}/src/reader/_app/cli.py +0 -0
  92. {reader-3.14 → reader-3.15}/src/reader/_app/static/controls.js +0 -0
  93. {reader-3.14 → reader-3.15}/src/reader/_app/static/style.css +0 -0
  94. {reader-3.14 → reader-3.15}/src/reader/_app/templates/add_entry.html +0 -0
  95. {reader-3.14 → reader-3.15}/src/reader/_app/templates/entries.html +0 -0
  96. {reader-3.14 → reader-3.15}/src/reader/_app/templates/entry.html +0 -0
  97. {reader-3.14 → reader-3.15}/src/reader/_app/templates/feeds.html +0 -0
  98. {reader-3.14 → reader-3.15}/src/reader/_app/templates/layout.html +0 -0
  99. {reader-3.14 → reader-3.15}/src/reader/_app/templates/macros.html +0 -0
  100. {reader-3.14 → reader-3.15}/src/reader/_app/templates/metadata.html +0 -0
  101. {reader-3.14 → reader-3.15}/src/reader/_app/templates/tags.html +0 -0
  102. {reader-3.14 → reader-3.15}/src/reader/_app/wsgi.py +0 -0
  103. {reader-3.14 → reader-3.15}/src/reader/_config.py +0 -0
  104. {reader-3.14 → reader-3.15}/src/reader/_hash_utils.py +0 -0
  105. {reader-3.14 → reader-3.15}/src/reader/_parser/_url_utils.py +0 -0
  106. {reader-3.14 → reader-3.15}/src/reader/_plugins/__init__.py +0 -0
  107. {reader-3.14 → reader-3.15}/src/reader/_plugins/cli_status.py +0 -0
  108. {reader-3.14 → reader-3.15}/src/reader/_plugins/enclosure_tags.py +0 -0
  109. {reader-3.14 → reader-3.15}/src/reader/_plugins/preview_feed_list.py +0 -0
  110. {reader-3.14 → reader-3.15}/src/reader/_plugins/share.py +0 -0
  111. {reader-3.14 → reader-3.15}/src/reader/_plugins/sqlite_releases.py +0 -0
  112. {reader-3.14 → reader-3.15}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  113. {reader-3.14 → reader-3.15}/src/reader/_plugins/timer.py +0 -0
  114. {reader-3.14 → reader-3.15}/src/reader/_storage/__init__.py +0 -0
  115. {reader-3.14 → reader-3.15}/src/reader/_storage/_changes.py +0 -0
  116. {reader-3.14 → reader-3.15}/src/reader/_storage/_entries.py +0 -0
  117. {reader-3.14 → reader-3.15}/src/reader/_storage/_html_utils.py +0 -0
  118. {reader-3.14 → reader-3.15}/src/reader/_storage/_sql_utils.py +0 -0
  119. {reader-3.14 → reader-3.15}/src/reader/_storage/_sqlite_utils.py +0 -0
  120. {reader-3.14 → reader-3.15}/src/reader/_vendor/__init__.py +0 -0
  121. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/__init__.py +0 -0
  122. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/api.py +0 -0
  123. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  124. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  125. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  126. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  127. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  128. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  129. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  130. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  131. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  132. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/encodings.py +0 -0
  133. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  134. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/html.py +0 -0
  135. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/http.py +0 -0
  136. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/mixin.py +0 -0
  137. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  138. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  139. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  140. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  141. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  142. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  143. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  144. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  145. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  146. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  147. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  148. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  149. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  150. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/py.typed +0 -0
  151. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  152. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/sgml.py +0 -0
  153. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/urls.py +0 -0
  154. {reader-3.14 → reader-3.15}/src/reader/_vendor/feedparser/util.py +0 -0
  155. {reader-3.14 → reader-3.15}/src/reader/plugins/enclosure_dedupe.py +0 -0
  156. {reader-3.14 → reader-3.15}/src/reader/plugins/ua_fallback.py +0 -0
  157. {reader-3.14 → reader-3.15}/src/reader/py.typed +0 -0
  158. {reader-3.14 → reader-3.15}/src/reader.egg-info/dependency_links.txt +0 -0
  159. {reader-3.14 → reader-3.15}/src/reader.egg-info/requires.txt +0 -0
  160. {reader-3.14 → reader-3.15}/src/reader.egg-info/top_level.txt +0 -0
  161. {reader-3.14 → reader-3.15}/tests/conftest.py +0 -0
  162. {reader-3.14 → reader-3.15}/tests/data/10.json +0 -0
  163. {reader-3.14 → reader-3.15}/tests/data/10.json.py +0 -0
  164. {reader-3.14 → reader-3.15}/tests/data/custom +0 -0
  165. {reader-3.14 → reader-3.15}/tests/data/empty.atom +0 -0
  166. {reader-3.14 → reader-3.15}/tests/data/empty.atom.py +0 -0
  167. {reader-3.14 → reader-3.15}/tests/data/empty.json +0 -0
  168. {reader-3.14 → reader-3.15}/tests/data/empty.json.py +0 -0
  169. {reader-3.14 → reader-3.15}/tests/data/empty.rss +0 -0
  170. {reader-3.14 → reader-3.15}/tests/data/empty.rss.py +0 -0
  171. {reader-3.14 → reader-3.15}/tests/data/full.atom +0 -0
  172. {reader-3.14 → reader-3.15}/tests/data/full.atom.py +0 -0
  173. {reader-3.14 → reader-3.15}/tests/data/full.json +0 -0
  174. {reader-3.14 → reader-3.15}/tests/data/full.json.py +0 -0
  175. {reader-3.14 → reader-3.15}/tests/data/full.rss +0 -0
  176. {reader-3.14 → reader-3.15}/tests/data/full.rss.py +0 -0
  177. {reader-3.14 → reader-3.15}/tests/data/invalid.json +0 -0
  178. {reader-3.14 → reader-3.15}/tests/data/invalid.json.py +0 -0
  179. {reader-3.14 → reader-3.15}/tests/data/relative.atom +0 -0
  180. {reader-3.14 → reader-3.15}/tests/data/relative.atom.py +0 -0
  181. {reader-3.14 → reader-3.15}/tests/data/relative.rss +0 -0
  182. {reader-3.14 → reader-3.15}/tests/data/relative.rss.py +0 -0
  183. {reader-3.14 → reader-3.15}/tests/data/sqlite_releases.html +0 -0
  184. {reader-3.14 → reader-3.15}/tests/data/unknown.json +0 -0
  185. {reader-3.14 → reader-3.15}/tests/data/unknown.json.py +0 -0
  186. {reader-3.14 → reader-3.15}/tests/reader_methods.py +0 -0
  187. {reader-3.14 → reader-3.15}/tests/reader_test_plugins/__init__.py +0 -0
  188. {reader-3.14 → reader-3.15}/tests/reader_test_plugins/good.py +0 -0
  189. {reader-3.14 → reader-3.15}/tests/reader_test_plugins/init_error.py +0 -0
  190. {reader-3.14 → reader-3.15}/tests/reader_test_plugins/missing_dependency.py +0 -0
  191. {reader-3.14 → reader-3.15}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  192. {reader-3.14 → reader-3.15}/tests/test__types.py +0 -0
  193. {reader-3.14 → reader-3.15}/tests/test_app.py +0 -0
  194. {reader-3.14 → reader-3.15}/tests/test_app_wsgi.py +0 -0
  195. {reader-3.14 → reader-3.15}/tests/test_changes.py +0 -0
  196. {reader-3.14 → reader-3.15}/tests/test_config.py +0 -0
  197. {reader-3.14 → reader-3.15}/tests/test_exceptions.py +0 -0
  198. {reader-3.14 → reader-3.15}/tests/test_hash_utils.py +0 -0
  199. {reader-3.14 → reader-3.15}/tests/test_html_utils.py +0 -0
  200. {reader-3.14 → reader-3.15}/tests/test_plugins_cli_status.py +0 -0
  201. {reader-3.14 → reader-3.15}/tests/test_plugins_enclosure_dedupe.py +0 -0
  202. {reader-3.14 → reader-3.15}/tests/test_plugins_entry_dedupe.py +0 -0
  203. {reader-3.14 → reader-3.15}/tests/test_plugins_mark_as_read.py +0 -0
  204. {reader-3.14 → reader-3.15}/tests/test_plugins_preview_feed_list.py +0 -0
  205. {reader-3.14 → reader-3.15}/tests/test_plugins_readtime.py +0 -0
  206. {reader-3.14 → reader-3.15}/tests/test_plugins_ua_fallback.py +0 -0
  207. {reader-3.14 → reader-3.15}/tests/test_reader_context.py +0 -0
  208. {reader-3.14 → reader-3.15}/tests/test_reader_counts.py +0 -0
  209. {reader-3.14 → reader-3.15}/tests/test_reader_deprecations.py +0 -0
  210. {reader-3.14 → reader-3.15}/tests/test_reader_filter.py +0 -0
  211. {reader-3.14 → reader-3.15}/tests/test_reader_hooks.py +0 -0
  212. {reader-3.14 → reader-3.15}/tests/test_reader_integration.py +0 -0
  213. {reader-3.14 → reader-3.15}/tests/test_reader_plugins.py +0 -0
  214. {reader-3.14 → reader-3.15}/tests/test_reader_search.py +0 -0
  215. {reader-3.14 → reader-3.15}/tests/test_reader_sort.py +0 -0
  216. {reader-3.14 → reader-3.15}/tests/test_reader_update.py +0 -0
  217. {reader-3.14 → reader-3.15}/tests/test_search.py +0 -0
  218. {reader-3.14 → reader-3.15}/tests/test_sql_utils.py +0 -0
  219. {reader-3.14 → reader-3.15}/tests/test_sqlite_utils.py +0 -0
  220. {reader-3.14 → reader-3.15}/tests/test_storage.py +0 -0
  221. {reader-3.14 → reader-3.15}/tests/test_tags.py +0 -0
  222. {reader-3.14 → reader-3.15}/tests/test_test_utils.py +0 -0
  223. {reader-3.14 → reader-3.15}/tests/test_types.py +0 -0
  224. {reader-3.14 → reader-3.15}/tests/test_utils.py +0 -0
  225. {reader-3.14 → reader-3.15}/tests/utils.py +0 -0
  226. {reader-3.14 → reader-3.15}/tox.ini +0 -0
@@ -3,7 +3,42 @@ Changelog
3
3
  =========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
+
8
+
9
+ Version 3.15
10
+ ------------
11
+
12
+ Released 2024-11-11
13
+
14
+ * During :ref:`scheduled updates <scheduled>`,
15
+ honor the Retry-After HTTP header sent with
16
+ 429 Too Many Requests or 503 Service Unavailable responses.
17
+ (:issue:`307`)
18
+ * Optimize OR-only :meth:`~Reader.get_feeds()` and :meth:`~Reader.get_entries()`
19
+ tag filters (e.g. ``[['one', ...]]``). (:issue:`359`)
20
+ * Expose :data:`reader.core.DEFAULT_RESERVED_NAME_SCHEME`. (:issue:`287`)
21
+
22
+ * Update the (unstable) parser API to expose HTTP information to the updater.
23
+ (:issue:`307`)
24
+
25
+ * The :class:`.RetrieverType` protocol used by retrievers changed
26
+ (new return type, allow storing arbitrary caching data via
27
+ :attr:`~.RetrievedFeed.caching_info`).
28
+ * The ``HTTPAcceptParserType`` was renamed to :class:`.AcceptParserType`.
29
+
30
+ * Allow :ref:`using the installed feedparser <no-vendored-feedparser>`
31
+ instead of the one vendored by *reader*.
32
+ This is useful for working around issues in the vendored feedparser,
33
+ like the libxml2 <=2.13.3 incompatibility reported in :issue:`350`;
34
+ thanks to `Maks Verver`_ for reporting,
35
+ root causing, and following up with both dependencies.
36
+
37
+ * Fix a number of brittle / broken tests.
38
+ Thanks to `Maks Verver`_ for the issues and fixes.
39
+ (:issue:`348`, :issue:`349`, :issue:`355`)
40
+
41
+ .. _Maks Verver: https://github.com/maksverver
7
42
 
8
43
 
9
44
  Version 3.14
@@ -650,6 +685,8 @@ Released 2022-03-12
650
685
  Fix broken error flashing.
651
686
 
652
687
 
688
+ .. _version 2.9:
689
+
653
690
  Version 2.9
654
691
  -----------
655
692
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.14
3
+ Version: 3.15
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -16,7 +16,7 @@ Most of *reader*'s functionality can be accessed through a :class:`Reader` insta
16
16
 
17
17
  Split this into sub-sections.
18
18
 
19
- .. autofunction:: make_reader(url, *, feed_root=None, plugins=..., session_timeout=(3.05, 60), reserved_name_scheme=..., search_enabled='auto')
19
+ .. autofunction:: make_reader
20
20
 
21
21
  .. autoclass:: Reader(...)
22
22
  :members:
@@ -184,4 +184,6 @@ Type aliases
184
184
  Constants
185
185
  ---------
186
186
 
187
+ .. autodata:: reader.core.DEFAULT_RESERVED_NAME_SCHEME
188
+
187
189
  .. autodata:: reader.plugins.DEFAULT_PLUGINS
@@ -3,7 +3,7 @@ Development
3
3
  ===========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  Development should follow a problem-solution_ approach.
@@ -378,8 +378,14 @@ Requests session plugins:
378
378
  Retriever / parser split:
379
379
 
380
380
  * :issue:`205#issuecomment-766321855`
381
- * split exception hierarchy (not implemented as of 3.9): :issue:`218#issuecomment-1687094315`
381
+ * split exception hierarchy (not implemented as of 3.15): :issue:`218#issuecomment-1687094315`
382
+ * API overview as of 3.14, meant to show the dataflow: :issue:`307#issuecomment-2266647310`
383
+ * ~ideal API (mostly implemented in 3.15): :issue:`307#issuecomment-2281797898`
382
384
 
385
+ * exposes HTTP information (so it can be used by the updater)
386
+ * introduces ~internal RetrieveError and NotModified :exc:`ParseError` subclasses;
387
+ notably, this doesn't really follow the split exception hierarchy mentioned above,
388
+ and is only meant to surface HTTP information in error cases
383
389
 
384
390
  Alternative feed parsers:
385
391
 
@@ -398,17 +404,13 @@ Lessons learned from the :ref:`Twitter` plugin:
398
404
  :meth:`~reader._parser.ParserType` being generic.
399
405
 
400
406
  * It is useful for a Retriever to store arbitrary caching data;
401
- the plugin (mis)used
402
- :attr:`~reader._parser.RetrieveResult.http_etag`
407
+ the plugin (mis)used ``RetrieveResult.http_etag``
403
408
  to store the (integer) id of the newest tweet in the thread.
404
409
 
405
- It would be nice to formalize this into a single
406
- "arbitrary caching data" attribute;
407
- also see `this comment <https://github.com/lemon24/reader/blob/0ada24bf4e65c69c3028641bada8eaeabbe02754/src/reader/_parser.py#L569>`_.
410
+ Update: This was formalized as :attr:`.RetrievedFeed.caching_info` in 3.15.
408
411
 
409
412
  * It is useful for a Retriever to pass arbitrary data to itself;
410
- the plugin (mis)used
411
- :attr:`~reader._types.FeedForUpdate.http_etag` to pass from
413
+ the plugin (mis)used ``FeedForUpdate.http_etag`` to pass from
412
414
  :meth:`~reader._parser.FeedForUpdateRetrieverType.process_feed_for_update`
413
415
  to :meth:`~reader._parser.RetrieverType.__call__`:
414
416
 
@@ -684,6 +686,14 @@ Update hook error handling:
684
686
  * expected behavior: :issue:`218#issuecomment-1595691410`, :issue:`218#issuecomment-1666823222`
685
687
  * update hook pseudocode + exception hierarchy: :issue:`218#issuecomment-1666869215`
686
688
 
689
+ Considerations on using pluggy: :issue:`329`,
690
+ `how adapting reader use cases to the pluggy model may work <https://github.com/pytest-dev/pluggy/issues/151#issuecomment-2421648901>`_.
691
+ Conclusions:
692
+
693
+ even if using pluggy would require some work-arounds for calling, I think the collection / discovery functionality is still more than worth it
694
+
695
+ long-term and/or if reader becomes really popular, pluggy is definitely the way to go
696
+
687
697
 
688
698
  Reserved names
689
699
  ~~~~~~~~~~~~~~
@@ -3,7 +3,7 @@ User guide
3
3
  ==========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  This page gives a tour of *reader*'s features,
@@ -272,34 +272,42 @@ Scheduled updates
272
272
  Because different feeds need to be updated at different rates,
273
273
  *reader* also provides a mechanism for scheduling updates.
274
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>`
275
+ On every update, *reader* determines when a feed should be updated next
276
+ based on a configurable update interval;
277
+ :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
278
278
  updates only the feeds that should be updated at or before the current time.
279
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.
280
+ The interval can be **configured by the user**
281
+ through the ``.reader.update`` global tag,
282
+ and overridden for individual feeds through the matching feed tag;
283
+ if no configuration is provided, the default interval is one hour.
284
+ See :data:`~reader.types.UpdateConfig` for the config format.
287
285
 
288
- .. note::
286
+ In addition, the user can specify a jitter,
287
+ a random amount of time added to the interval
288
+ such that updates don't happen exactly at the the same time;
289
+ for example, with a one-hour interval, a jitter of 0.25 means
290
+ updates will occur any time in the first 15 minutes of each hour.
291
+
292
+ If the server sends the Retry-After HTTP header with
293
+ 429 Too Many Requests or 503 Service Unavailable responses,
294
+ :meth:`update_feeds(scheduled=True) <Reader.update_feeds>` will honor it.
289
295
 
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
 
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.
297
+ .. note::
298
+
299
+ As of |version|, there is no way to enforce a minimum update interval;
300
+ if you want feeds to be updated no more often than e.g. every hour,
301
+ you have to run :meth:`~Reader.update_feeds` no more often than every hour.
302
+ Please :ref:`open an issue <issues>` or :ref:`submit a pull request <prs>`
303
+ if you need a minimum update interval.
299
304
 
300
305
 
301
306
  .. versionadded:: 3.13
302
307
 
308
+ .. versionchanged:: 3.15
309
+ Honor the Retry-After HTTP header.
310
+
303
311
 
304
312
  Update status
305
313
  ~~~~~~~~~~~~~
@@ -27,6 +27,20 @@ These packages will be installed automatically when installing *reader*:
27
27
  (at least SQLite 3.18 with the `JSON1`_ and `FTS5`_ extensions).
28
28
 
29
29
 
30
+ .. _no-vendored-feedparser:
31
+
32
+ .. note::
33
+
34
+ Because `feedparser`_ makes PyPI releases at a lower cadence,
35
+ *reader* uses a vendored version of feedparser's `develop`_ branch
36
+ by default since :ref:`version 2.9`.
37
+ To opt out of this behavior, and make *reader* use
38
+ the installed ``feedparser`` package,
39
+ set the ``READER_NO_VENDORED_FEEDPARSER`` environment variable to ``1``.
40
+
41
+ .. _develop: https://github.com/kurtmckee/feedparser
42
+
43
+
30
44
  .. _optional dependencies:
31
45
 
32
46
  Optional dependencies
@@ -45,7 +45,7 @@ Protocols
45
45
  ~~~~~~~~~
46
46
 
47
47
  .. module:: reader._parser
48
- :noindex:
48
+ :no-index:
49
49
 
50
50
  .. autoclass:: FeedArgument
51
51
  :members:
@@ -54,6 +54,14 @@ Protocols
54
54
  :members:
55
55
  :special-members: __call__
56
56
 
57
+ .. autoexception:: RetrieveError
58
+ :show-inheritance:
59
+ :members:
60
+
61
+ .. autoexception:: NotModified
62
+ :show-inheritance:
63
+ :members:
64
+
57
65
  .. autoclass:: FeedForUpdateRetrieverType
58
66
  :members:
59
67
  :show-inheritance:
@@ -62,7 +70,7 @@ Protocols
62
70
  :members:
63
71
  :special-members: __call__
64
72
 
65
- .. autoclass:: HTTPAcceptParserType
73
+ .. autoclass:: AcceptParserType
66
74
  :members:
67
75
  :show-inheritance:
68
76
 
@@ -72,7 +80,7 @@ Protocols
72
80
 
73
81
 
74
82
  .. module:: reader._parser.requests
75
- :noindex:
83
+ :no-index:
76
84
 
77
85
  .. autoclass:: RequestHook
78
86
  :members:
@@ -87,30 +95,21 @@ Data objects
87
95
  ~~~~~~~~~~~~
88
96
 
89
97
  .. module:: reader._parser
90
- :noindex:
98
+ :no-index:
91
99
 
92
100
  .. autoclass:: RetrieveResult
93
101
  :members:
94
102
 
95
- .. module:: reader._types
96
-
97
- .. autoclass:: ParsedFeed
103
+ .. autoclass:: RetrievedFeed
98
104
  :members:
99
105
 
100
- .. autoclass:: FeedData
101
- :members:
102
- :undoc-members:
103
-
104
- .. autoclass:: EntryData
106
+ .. autoclass:: ParseResult
105
107
  :members:
106
- :undoc-members:
107
108
 
108
- .. todo:: the following should be in the storage section, when we get one
109
-
110
- .. autoclass:: FeedForUpdate
109
+ .. autoclass:: ParsedFeed
111
110
  :members:
112
111
 
113
- .. autoclass:: EntryForUpdate
112
+ .. autoclass:: HTTPInfo
114
113
  :members:
115
114
 
116
115
 
@@ -155,7 +154,7 @@ but at least one other implementation needs to exists before that.
155
154
  .. autoattribute:: reader.Reader._search
156
155
 
157
156
  .. module:: reader._types
158
- :noindex:
157
+ :no-index:
159
158
 
160
159
  .. autoclass:: StorageType()
161
160
  :members:
@@ -171,6 +170,9 @@ but at least one other implementation needs to exists before that.
171
170
 
172
171
  .. _changes:
173
172
 
173
+ Change tracking
174
+ ~~~~~~~~~~~~~~~
175
+
174
176
  .. autoclass:: ChangeTrackingStorageType()
175
177
  :members:
176
178
  :show-inheritance:
@@ -193,12 +195,26 @@ but at least one other implementation needs to exists before that.
193
195
  Data objects
194
196
  ~~~~~~~~~~~~
195
197
 
198
+ .. autoclass:: FeedData
199
+ :members:
200
+ :undoc-members:
201
+
202
+ .. autoclass:: EntryData
203
+ :members:
204
+ :undoc-members:
205
+
196
206
  .. autoclass:: FeedFilter
197
207
  :members:
198
208
 
199
209
  .. autoclass:: EntryFilter
200
210
  :members:
201
211
 
212
+ .. autoclass:: FeedForUpdate
213
+ :members:
214
+
215
+ .. autoclass:: EntryForUpdate
216
+ :members:
217
+
202
218
  .. autoclass:: FeedUpdateIntent
203
219
  :members:
204
220
 
@@ -3,7 +3,7 @@ Plugins
3
3
  =======
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  .. _built-in plugins:
@@ -116,3 +116,20 @@ For `built-in plugins`_, it is enough to use the plugin name (``reader.XYZ``).
116
116
  To load web application plugins, set the ``READER_APP_PLUGIN`` environment variable.
117
117
  To load CLI plugins (that customize the CLI),
118
118
  set the ``READER_CLI_PLUGIN`` environment variable.
119
+
120
+
121
+
122
+ Recipes
123
+ -------
124
+
125
+ I currently don't need this functionality,
126
+ but if you'd be interested in maintaining any of these
127
+ as an experimental or even built-in plugin,
128
+ please :doc:`submit a pull request <contributing>`.
129
+
130
+ .. include:: ../examples/feed_slugs.py
131
+ :start-after: """
132
+ :end-before: """
133
+ .. literalinclude:: ../examples/feed_slugs.py
134
+ :start-at: def init_reader
135
+ :end-before: if __name__
@@ -2,7 +2,7 @@ Tutorial
2
2
  ========
3
3
 
4
4
  .. module:: reader
5
- :noindex:
5
+ :no-index:
6
6
 
7
7
 
8
8
  In this tutorial we'll use *reader* to download all the episodes of a podcast,
@@ -0,0 +1,95 @@
1
+ """
2
+ Feed slugs
3
+ ~~~~~~~~~~
4
+
5
+ This is a recipe of what a "get feed by slug" plugin may look like
6
+ (e.g. for user-defined short URLs).
7
+
8
+ Usage::
9
+
10
+ >>> from reader import make_reader
11
+ >>> import feed_slugs
12
+ >>> reader = make_reader('db.sqlite', plugins=[feed_slugs.init_reader])
13
+ >>> reader.set_feed_slug('https://death.andgravity.com/_feed/index.xml', 'andgravity')
14
+ >>> reader.get_feed_by_slug('andgravity')
15
+ Feed(url='https://death.andgravity.com/_feed/index.xml', ...)
16
+ >>> reader.get_feed_slug(_.url)
17
+ 'andgravity'
18
+
19
+ ..
20
+ Originally implemented for https://github.com/lemon24/reader/issues/358.
21
+
22
+ """
23
+
24
+ # fmt: off
25
+ # flake8: noqa
26
+
27
+ def init_reader(reader):
28
+ # __get__() allows help(reader.get_feed_by_slug) to work
29
+ reader.get_feed_by_slug = get_feed_by_slug.__get__(reader)
30
+ reader.get_feed_slug = get_feed_slug.__get__(reader)
31
+ reader.set_feed_slug = set_feed_slug.__get__(reader)
32
+
33
+ def get_feed_by_slug(reader, slug):
34
+ tag = _make_tag(reader, slug)
35
+ return next(reader.get_feeds(tags=[tag], limit=1), None)
36
+
37
+ def get_feed_slug(reader, feed):
38
+ if tag := next(_get_tags(reader, feed), None):
39
+ return tag.removeprefix(_make_tag(reader, ''))
40
+ return None
41
+
42
+ def set_feed_slug(reader, feed, slug: str | None):
43
+ feed = reader.get_feed(feed)
44
+ tag = _make_tag(reader, slug)
45
+
46
+ if not slug:
47
+ reader.delete_tag(feed, tag, missing_ok=True)
48
+ return
49
+
50
+ reader.set_tag(feed, tag)
51
+
52
+ # ensure only one feed has the slug; technically a race condition,
53
+ # when it happens no feed will have the tag
54
+ for other_feed in reader.get_feeds(tags=[tag]):
55
+ if feed.url != other_feed.url:
56
+ reader.delete_tag(other_feed, tag, missing_ok=True)
57
+
58
+ # ensure feed has only one slug; technically a race condition,
59
+ # when it happens the feed will have no slug
60
+ for other_tag in _get_tags(reader, feed):
61
+ if tag != other_tag:
62
+ reader.delete_tag(feed, other_tag, missing_ok=True)
63
+
64
+ def _make_tag(reader, slug):
65
+ return reader.make_plugin_reserved_name('slug', slug)
66
+
67
+ def _get_tags(reader, resource):
68
+ prefix = _make_tag(reader, '')
69
+ # filter tags by prefix would make this faster,
70
+ # https://github.com/lemon24/reader/issues/309
71
+ return (t for t in reader.get_tag_keys(resource) if t.startswith(prefix))
72
+
73
+ if __name__ == '__main__':
74
+ from reader import make_reader
75
+
76
+ reader = make_reader('db.sqlite', plugins=[init_reader])
77
+ url = 'https://death.andgravity.com/_feed/index.xml'
78
+
79
+ reader.set_feed_slug(url, 'one')
80
+ print(
81
+ reader.get_feed_slug(url),
82
+ getattr(reader.get_feed_by_slug('one'), 'url', None),
83
+ )
84
+
85
+ reader.set_feed_slug(url, 'two')
86
+ print(
87
+ reader.get_feed_slug(url),
88
+ getattr(reader.get_feed_by_slug('two'), 'url', None),
89
+ )
90
+
91
+ reader.set_feed_slug('https://xkcd.com/atom.xml', 'two')
92
+ print(
93
+ reader.get_feed_slug(url),
94
+ getattr(reader.get_feed_by_slug('two'), 'url', None),
95
+ )
@@ -1,13 +1,13 @@
1
1
  #!/bin/bash
2
2
  #
3
- # back up a SQLite database not owned by the current user
3
+ # back up a SQLite database
4
4
  #
5
5
  # usage:
6
- # backup.sh src dst
7
- # backup.sh src
6
+ # ./backup.sh src dst
7
+ # ./backup.sh src
8
8
  #
9
9
  # example:
10
- # "backup.sh /src/db.sqlite" -> ./db.sqlite.2023-01-28
10
+ # "./backup.sh /src/db.sqlite" -> ./db.sqlite.2023-01-28.gz
11
11
  #
12
12
 
13
13
  set -o nounset
@@ -29,5 +29,8 @@ trap 'rm -rf '"$tmpdir" EXIT
29
29
 
30
30
  tmp=$tmpdir/$( basename "$src" )
31
31
 
32
- time sudo sqlite3 "$src" "VACUUM INTO '$tmp'"
33
- time cp "$tmp" "$dst"
32
+ du -sh "$src"
33
+ sqlite3 "$src" "VACUUM INTO '$tmp'"
34
+ du -sh "$tmp"
35
+ gzip -c "$tmp" > "$dst.gz"
36
+ du -sh "$dst.gz"
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.14'
40
+ __version__ = '3.15'
41
41
 
42
42
  # isort: off
43
43
 
@@ -122,13 +122,12 @@ def log_command(fn):
122
122
 
123
123
  def config_option(*args, **kwargs):
124
124
  def callback(ctx, param, value):
125
- # TODO: the default file is allowed to not exist, a user specified file must exist
125
+ config_path = value if value is not None else get_default_config_path()
126
126
  try:
127
- with open(value) as file:
127
+ with open(config_path) as file:
128
128
  config = make_reader_config(yaml.safe_load(file))
129
- except FileNotFoundError as e:
130
- if value != param.default:
131
- raise click.BadParameter(str(e), ctx=ctx, param=param) from None
129
+ except FileNotFoundError:
130
+ assert value is None
132
131
  config = make_reader_config({})
133
132
 
134
133
  ctx.default_map = config['cli'].get('defaults', {})
@@ -136,17 +135,14 @@ def config_option(*args, **kwargs):
136
135
  ctx.obj = config
137
136
  return config
138
137
 
139
- def inner(fn):
140
- return click.option(
141
- *args,
142
- type=click.Path(dir_okay=False),
143
- callback=callback,
144
- is_eager=True,
145
- expose_value=False,
146
- **kwargs,
147
- )(fn)
148
-
149
- return inner
138
+ return click.option(
139
+ *args,
140
+ type=click.Path(exists=True, dir_okay=False),
141
+ callback=callback,
142
+ is_eager=True,
143
+ expose_value=False,
144
+ **kwargs,
145
+ )
150
146
 
151
147
 
152
148
  def pass_reader(fn):
@@ -166,7 +162,6 @@ def pass_reader(fn):
166
162
  '--db',
167
163
  type=click.Path(dir_okay=False),
168
164
  envvar=reader._DB_ENVVAR,
169
- show_default=True,
170
165
  help=f"Path to the reader database. [default: {get_default_db_path()}]",
171
166
  )
172
167
  @click.option(
@@ -184,9 +179,7 @@ def pass_reader(fn):
184
179
  @config_option(
185
180
  '--config',
186
181
  envvar=reader._CONFIG_ENVVAR,
187
- help="Path to the reader config.",
188
- default=get_default_config_path(),
189
- show_default=True,
182
+ help=f"Path to the reader config. [default: {get_default_config_path()}]",
190
183
  )
191
184
  @click.option(
192
185
  '--feed-root',