reader 3.14__tar.gz → 3.16__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 (228) hide show
  1. {reader-3.14 → reader-3.16}/CHANGES.rst +92 -1
  2. {reader-3.14/src/reader.egg-info → reader-3.16}/PKG-INFO +1 -1
  3. {reader-3.14 → reader-3.16}/docs/api.rst +13 -1
  4. {reader-3.14 → reader-3.16}/docs/dev.rst +36 -14
  5. {reader-3.14 → reader-3.16}/docs/guide.rst +28 -20
  6. {reader-3.14 → reader-3.16}/docs/install.rst +14 -0
  7. {reader-3.14 → reader-3.16}/docs/internal.rst +34 -18
  8. {reader-3.14 → reader-3.16}/docs/plugins.rst +18 -1
  9. {reader-3.14 → reader-3.16}/docs/tutorial.rst +1 -1
  10. reader-3.16/examples/feed_slugs.py +95 -0
  11. {reader-3.14 → reader-3.16}/scripts/backup.sh +9 -6
  12. {reader-3.14 → reader-3.16}/src/reader/__init__.py +2 -1
  13. {reader-3.14 → reader-3.16}/src/reader/_app/__init__.py +15 -19
  14. {reader-3.14 → reader-3.16}/src/reader/_app/templates/entries.html +4 -1
  15. {reader-3.14 → reader-3.16}/src/reader/_app/templates/entry.html +1 -1
  16. {reader-3.14 → reader-3.16}/src/reader/_app/templates/macros.html +2 -8
  17. {reader-3.14 → reader-3.16}/src/reader/_cli.py +13 -20
  18. {reader-3.14 → reader-3.16}/src/reader/_parser/__init__.py +187 -83
  19. {reader-3.14 → reader-3.16}/src/reader/_parser/_http_utils.py +1 -0
  20. {reader-3.14 → reader-3.16}/src/reader/_parser/_lazy.py +166 -137
  21. {reader-3.14 → reader-3.16}/src/reader/_parser/feedparser.py +29 -3
  22. {reader-3.14 → reader-3.16}/src/reader/_parser/file.py +2 -6
  23. reader-3.16/src/reader/_parser/http.py +107 -0
  24. {reader-3.14 → reader-3.16}/src/reader/_parser/jsonfeed.py +1 -1
  25. {reader-3.14 → reader-3.16}/src/reader/_parser/requests/__init__.py +2 -0
  26. {reader-3.14 → reader-3.16}/src/reader/_parser/requests/_lazy.py +26 -6
  27. {reader-3.14 → reader-3.16}/src/reader/_storage/_base.py +13 -12
  28. {reader-3.14 → reader-3.16}/src/reader/_storage/_entries.py +34 -3
  29. {reader-3.14 → reader-3.16}/src/reader/_storage/_feeds.py +10 -9
  30. {reader-3.14 → reader-3.16}/src/reader/_storage/_schema.py +57 -3
  31. {reader-3.14 → reader-3.16}/src/reader/_storage/_search.py +50 -13
  32. {reader-3.14 → reader-3.16}/src/reader/_storage/_sqlite_utils.py +10 -1
  33. {reader-3.14 → reader-3.16}/src/reader/_storage/_tags.py +158 -52
  34. {reader-3.14 → reader-3.16}/src/reader/_types.py +108 -82
  35. {reader-3.14 → reader-3.16}/src/reader/_update.py +57 -54
  36. {reader-3.14 → reader-3.16}/src/reader/_utils.py +16 -3
  37. {reader-3.14 → reader-3.16}/src/reader/core.py +126 -44
  38. {reader-3.14 → reader-3.16}/src/reader/exceptions.py +6 -10
  39. {reader-3.14 → reader-3.16}/src/reader/plugins/__init__.py +1 -1
  40. {reader-3.14 → reader-3.16}/src/reader/plugins/entry_dedupe.py +1 -1
  41. {reader-3.14 → reader-3.16}/src/reader/plugins/mark_as_read.py +1 -1
  42. {reader-3.14 → reader-3.16}/src/reader/plugins/readtime.py +1 -1
  43. {reader-3.14 → reader-3.16}/src/reader/types.py +70 -5
  44. reader-3.16/src/reader/utils.py +78 -0
  45. {reader-3.14 → reader-3.16/src/reader.egg-info}/PKG-INFO +1 -1
  46. {reader-3.14 → reader-3.16}/src/reader.egg-info/SOURCES.txt +4 -1
  47. {reader-3.14 → reader-3.16}/tests/data/full.atom +38 -0
  48. {reader-3.14 → reader-3.16}/tests/data/full.atom.py +31 -0
  49. {reader-3.14 → reader-3.16}/tests/data/full.rss +14 -0
  50. {reader-3.14 → reader-3.16}/tests/data/full.rss.py +16 -0
  51. {reader-3.14 → reader-3.16}/tests/data/relative.rss +1 -1
  52. {reader-3.14 → reader-3.16}/tests/data/relative.rss.py +1 -1
  53. {reader-3.14 → reader-3.16}/tests/fakeparser.py +15 -16
  54. {reader-3.14 → reader-3.16}/tests/test__types.py +5 -2
  55. {reader-3.14 → reader-3.16}/tests/test_bench.py +1 -0
  56. {reader-3.14 → reader-3.16}/tests/test_cli.py +23 -8
  57. {reader-3.14 → reader-3.16}/tests/test_lazy_imports.py +11 -1
  58. {reader-3.14 → reader-3.16}/tests/test_parser.py +220 -89
  59. {reader-3.14 → reader-3.16}/tests/test_plugins_sqlite_releases.py +4 -2
  60. {reader-3.14 → reader-3.16}/tests/test_reader.py +161 -7
  61. {reader-3.14 → reader-3.16}/tests/test_reader_filter.py +5 -1
  62. {reader-3.14 → reader-3.16}/tests/test_reader_integration.py +5 -5
  63. {reader-3.14 → reader-3.16}/tests/test_reader_private.py +26 -20
  64. {reader-3.14 → reader-3.16}/tests/test_reader_search.py +90 -0
  65. reader-3.16/tests/test_reader_utils.py +59 -0
  66. {reader-3.14 → reader-3.16}/tests/test_storage.py +1 -6
  67. reader-3.14/src/reader/_parser/http.py +0 -108
  68. {reader-3.14 → reader-3.16}/LICENSE +0 -0
  69. {reader-3.14 → reader-3.16}/MANIFEST.in +0 -0
  70. {reader-3.14 → reader-3.16}/README.rst +0 -0
  71. {reader-3.14 → reader-3.16}/docs/Makefile +0 -0
  72. {reader-3.14 → reader-3.16}/docs/app.rst +0 -0
  73. {reader-3.14 → reader-3.16}/docs/changelog.rst +0 -0
  74. {reader-3.14 → reader-3.16}/docs/cli.rst +0 -0
  75. {reader-3.14 → reader-3.16}/docs/conf.py +0 -0
  76. {reader-3.14 → reader-3.16}/docs/config.rst +0 -0
  77. {reader-3.14 → reader-3.16}/docs/contributing.rst +0 -0
  78. {reader-3.14 → reader-3.16}/docs/dev-app.rst +0 -0
  79. {reader-3.14 → reader-3.16}/docs/images/redesign-01.png +0 -0
  80. {reader-3.14 → reader-3.16}/docs/index.rst +0 -0
  81. {reader-3.14 → reader-3.16}/docs/make.bat +0 -0
  82. {reader-3.14 → reader-3.16}/docs/screenshots/dillo.png +0 -0
  83. {reader-3.14 → reader-3.16}/docs/screenshots/entries-feed.png +0 -0
  84. {reader-3.14 → reader-3.16}/docs/screenshots/entries.png +0 -0
  85. {reader-3.14 → reader-3.16}/docs/screenshots/entry-one.png +0 -0
  86. {reader-3.14 → reader-3.16}/docs/screenshots/entry-two.png +0 -0
  87. {reader-3.14 → reader-3.16}/docs/screenshots/feeds.png +0 -0
  88. {reader-3.14 → reader-3.16}/docs/screenshots/lynx.png +0 -0
  89. {reader-3.14 → reader-3.16}/docs/screenshots/search.png +0 -0
  90. {reader-3.14 → reader-3.16}/docs/why.rst +0 -0
  91. {reader-3.14 → reader-3.16}/examples/config.yaml +0 -0
  92. {reader-3.14 → reader-3.16}/examples/custom_headers.py +0 -0
  93. {reader-3.14 → reader-3.16}/examples/parser_only.py +0 -0
  94. {reader-3.14 → reader-3.16}/examples/podcast.py +0 -0
  95. {reader-3.14 → reader-3.16}/examples/terminal.py +0 -0
  96. {reader-3.14 → reader-3.16}/pyproject.toml +0 -0
  97. {reader-3.14 → reader-3.16}/run.sh +0 -0
  98. {reader-3.14 → reader-3.16}/scripts/.coverage +0 -0
  99. {reader-3.14 → reader-3.16}/scripts/bench.py +0 -0
  100. {reader-3.14 → reader-3.16}/scripts/debug_storage_stats.py +0 -0
  101. {reader-3.14 → reader-3.16}/scripts/generate_import_all.py +0 -0
  102. {reader-3.14 → reader-3.16}/scripts/jscontrols.html +0 -0
  103. {reader-3.14 → reader-3.16}/scripts/jscontrols.py +0 -0
  104. {reader-3.14 → reader-3.16}/scripts/lines.sh +0 -0
  105. {reader-3.14 → reader-3.16}/scripts/release.py +0 -0
  106. {reader-3.14 → reader-3.16}/setup.cfg +0 -0
  107. {reader-3.14 → reader-3.16}/src/reader/__main__.py +0 -0
  108. {reader-3.14 → reader-3.16}/src/reader/_app/api_thing.py +0 -0
  109. {reader-3.14 → reader-3.16}/src/reader/_app/cli.py +0 -0
  110. {reader-3.14 → reader-3.16}/src/reader/_app/static/controls.js +0 -0
  111. {reader-3.14 → reader-3.16}/src/reader/_app/static/style.css +0 -0
  112. {reader-3.14 → reader-3.16}/src/reader/_app/templates/add_entry.html +0 -0
  113. {reader-3.14 → reader-3.16}/src/reader/_app/templates/feeds.html +0 -0
  114. {reader-3.14 → reader-3.16}/src/reader/_app/templates/layout.html +0 -0
  115. {reader-3.14 → reader-3.16}/src/reader/_app/templates/metadata.html +0 -0
  116. {reader-3.14 → reader-3.16}/src/reader/_app/templates/tags.html +0 -0
  117. {reader-3.14 → reader-3.16}/src/reader/_app/wsgi.py +0 -0
  118. {reader-3.14 → reader-3.16}/src/reader/_config.py +0 -0
  119. {reader-3.14 → reader-3.16}/src/reader/_hash_utils.py +0 -0
  120. {reader-3.14 → reader-3.16}/src/reader/_parser/_url_utils.py +0 -0
  121. {reader-3.14 → reader-3.16}/src/reader/_plugins/__init__.py +0 -0
  122. {reader-3.14 → reader-3.16}/src/reader/_plugins/cli_status.py +0 -0
  123. {reader-3.14 → reader-3.16}/src/reader/_plugins/enclosure_tags.py +0 -0
  124. {reader-3.14 → reader-3.16}/src/reader/_plugins/preview_feed_list.py +0 -0
  125. {reader-3.14 → reader-3.16}/src/reader/_plugins/share.py +0 -0
  126. {reader-3.14 → reader-3.16}/src/reader/_plugins/sqlite_releases.py +0 -0
  127. {reader-3.14 → reader-3.16}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  128. {reader-3.14 → reader-3.16}/src/reader/_plugins/timer.py +0 -0
  129. {reader-3.14 → reader-3.16}/src/reader/_storage/__init__.py +0 -0
  130. {reader-3.14 → reader-3.16}/src/reader/_storage/_changes.py +0 -0
  131. {reader-3.14 → reader-3.16}/src/reader/_storage/_html_utils.py +0 -0
  132. {reader-3.14 → reader-3.16}/src/reader/_storage/_sql_utils.py +0 -0
  133. {reader-3.14 → reader-3.16}/src/reader/_vendor/__init__.py +0 -0
  134. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/__init__.py +0 -0
  135. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/api.py +0 -0
  136. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  137. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  138. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  139. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  140. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  141. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  142. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  143. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  144. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  145. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/encodings.py +0 -0
  146. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  147. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/html.py +0 -0
  148. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/http.py +0 -0
  149. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/mixin.py +0 -0
  150. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  151. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  152. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  153. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  154. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  155. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  156. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  157. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  158. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  159. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  160. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  161. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  162. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  163. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/py.typed +0 -0
  164. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  165. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/sgml.py +0 -0
  166. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/urls.py +0 -0
  167. {reader-3.14 → reader-3.16}/src/reader/_vendor/feedparser/util.py +0 -0
  168. {reader-3.14 → reader-3.16}/src/reader/plugins/enclosure_dedupe.py +0 -0
  169. {reader-3.14 → reader-3.16}/src/reader/plugins/ua_fallback.py +0 -0
  170. {reader-3.14 → reader-3.16}/src/reader/py.typed +0 -0
  171. {reader-3.14 → reader-3.16}/src/reader.egg-info/dependency_links.txt +0 -0
  172. {reader-3.14 → reader-3.16}/src/reader.egg-info/requires.txt +0 -0
  173. {reader-3.14 → reader-3.16}/src/reader.egg-info/top_level.txt +0 -0
  174. {reader-3.14 → reader-3.16}/tests/conftest.py +0 -0
  175. {reader-3.14 → reader-3.16}/tests/data/10.json +0 -0
  176. {reader-3.14 → reader-3.16}/tests/data/10.json.py +0 -0
  177. {reader-3.14 → reader-3.16}/tests/data/custom +0 -0
  178. {reader-3.14 → reader-3.16}/tests/data/empty.atom +0 -0
  179. {reader-3.14 → reader-3.16}/tests/data/empty.atom.py +0 -0
  180. {reader-3.14 → reader-3.16}/tests/data/empty.json +0 -0
  181. {reader-3.14 → reader-3.16}/tests/data/empty.json.py +0 -0
  182. {reader-3.14 → reader-3.16}/tests/data/empty.rss +0 -0
  183. {reader-3.14 → reader-3.16}/tests/data/empty.rss.py +0 -0
  184. {reader-3.14 → reader-3.16}/tests/data/full.json +0 -0
  185. {reader-3.14 → reader-3.16}/tests/data/full.json.py +0 -0
  186. {reader-3.14 → reader-3.16}/tests/data/invalid.json +0 -0
  187. {reader-3.14 → reader-3.16}/tests/data/invalid.json.py +0 -0
  188. {reader-3.14 → reader-3.16}/tests/data/relative.atom +0 -0
  189. {reader-3.14 → reader-3.16}/tests/data/relative.atom.py +0 -0
  190. {reader-3.14 → reader-3.16}/tests/data/sqlite_releases.html +0 -0
  191. {reader-3.14 → reader-3.16}/tests/data/unknown.json +0 -0
  192. {reader-3.14 → reader-3.16}/tests/data/unknown.json.py +0 -0
  193. {reader-3.14 → reader-3.16}/tests/reader_methods.py +0 -0
  194. {reader-3.14 → reader-3.16}/tests/reader_test_plugins/__init__.py +0 -0
  195. {reader-3.14 → reader-3.16}/tests/reader_test_plugins/good.py +0 -0
  196. {reader-3.14 → reader-3.16}/tests/reader_test_plugins/init_error.py +0 -0
  197. {reader-3.14 → reader-3.16}/tests/reader_test_plugins/missing_dependency.py +0 -0
  198. {reader-3.14 → reader-3.16}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  199. /reader-3.14/tests/test_utils.py → /reader-3.16/tests/test__utils.py +0 -0
  200. {reader-3.14 → reader-3.16}/tests/test_app.py +0 -0
  201. {reader-3.14 → reader-3.16}/tests/test_app_wsgi.py +0 -0
  202. {reader-3.14 → reader-3.16}/tests/test_changes.py +0 -0
  203. {reader-3.14 → reader-3.16}/tests/test_config.py +0 -0
  204. {reader-3.14 → reader-3.16}/tests/test_exceptions.py +0 -0
  205. {reader-3.14 → reader-3.16}/tests/test_hash_utils.py +0 -0
  206. {reader-3.14 → reader-3.16}/tests/test_html_utils.py +0 -0
  207. {reader-3.14 → reader-3.16}/tests/test_plugins_cli_status.py +0 -0
  208. {reader-3.14 → reader-3.16}/tests/test_plugins_enclosure_dedupe.py +0 -0
  209. {reader-3.14 → reader-3.16}/tests/test_plugins_entry_dedupe.py +0 -0
  210. {reader-3.14 → reader-3.16}/tests/test_plugins_mark_as_read.py +0 -0
  211. {reader-3.14 → reader-3.16}/tests/test_plugins_preview_feed_list.py +0 -0
  212. {reader-3.14 → reader-3.16}/tests/test_plugins_readtime.py +0 -0
  213. {reader-3.14 → reader-3.16}/tests/test_plugins_ua_fallback.py +0 -0
  214. {reader-3.14 → reader-3.16}/tests/test_reader_context.py +0 -0
  215. {reader-3.14 → reader-3.16}/tests/test_reader_counts.py +0 -0
  216. {reader-3.14 → reader-3.16}/tests/test_reader_deprecations.py +0 -0
  217. {reader-3.14 → reader-3.16}/tests/test_reader_hooks.py +0 -0
  218. {reader-3.14 → reader-3.16}/tests/test_reader_plugins.py +0 -0
  219. {reader-3.14 → reader-3.16}/tests/test_reader_sort.py +0 -0
  220. {reader-3.14 → reader-3.16}/tests/test_reader_update.py +0 -0
  221. {reader-3.14 → reader-3.16}/tests/test_search.py +0 -0
  222. {reader-3.14 → reader-3.16}/tests/test_sql_utils.py +0 -0
  223. {reader-3.14 → reader-3.16}/tests/test_sqlite_utils.py +0 -0
  224. {reader-3.14 → reader-3.16}/tests/test_tags.py +0 -0
  225. {reader-3.14 → reader-3.16}/tests/test_test_utils.py +0 -0
  226. {reader-3.14 → reader-3.16}/tests/test_types.py +0 -0
  227. {reader-3.14 → reader-3.16}/tests/utils.py +0 -0
  228. {reader-3.14 → reader-3.16}/tox.ini +0 -0
@@ -3,7 +3,96 @@ Changelog
3
3
  =========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
+
8
+
9
+ Version 3.16
10
+ ------------
11
+
12
+ Released 2024-12-08
13
+
14
+ * Parse and store :attr:`Entry.source` for Atom and RSS feeds. (:issue:`267`)
15
+
16
+ * Add :attr:`Entry.feed_resolved_title` and :attr:`Feed.resolved_title` properties.
17
+ * The ``feed`` search column now indexes :attr:`Entry.feed_resolved_title`,
18
+ instead of feed :attr:`~Feed.user_title` or :attr:`~Feed.title`.
19
+ * Allow filtering entries by the entry source.
20
+
21
+ * Add :func:`reader.utils.archive_entries` for
22
+ copying entries to an "archived" feed.
23
+ (:issue:`290`)
24
+
25
+ * Add :meth:`~Reader.copy_entry`.
26
+ * Allow archiving entries from the web app.
27
+
28
+ * Fix bug causing :class:`Reader` operations
29
+ from a thread other than the one that created the instance
30
+ to happen with foreign key constraint enforcement disabled
31
+ (e.g. deleting a feed from another thread would not delete its entries).
32
+
33
+ This bug exists since using :class:`Reader` instances from other threads
34
+ became allowed in `2.15 <Version 2.15_>`_.
35
+
36
+ Serving the web application with ``python -m reader serve``
37
+ is known to be affected.
38
+ Serving it with uWSGI without threads (the default)
39
+ should not be affected.
40
+
41
+ .. attention::
42
+
43
+ **Your database may be in an inconsistent state because of this bug.**
44
+
45
+ It is recommended you run `PRAGMA foreign_key_check`_ on your database.
46
+
47
+ If you are upgrading from a version prior to 3.16
48
+ (i.e. were not using a pre-release version of *reader*),
49
+ the migration will do so for you.
50
+ If there are inconsistencies, you will get this error::
51
+
52
+ StorageError: integrity error: after migrating to version 43:
53
+ integrity error: FOREIGN KEY constraint failed
54
+
55
+ * Fix :meth:`~Reader.enable_search` / :meth:`~Reader.update_search`
56
+ not working when the search database is missing but change tracking is enabled
57
+ (e.g. when restoring the main database from backup).
58
+ (:issue:`362`)
59
+
60
+ .. _PRAGMA foreign_key_check: https://www.sqlite.org/pragma.html#pragma_foreign_key_check
61
+
62
+
63
+ Version 3.15
64
+ ------------
65
+
66
+ Released 2024-11-11
67
+
68
+ * During :ref:`scheduled updates <scheduled>`,
69
+ honor the Retry-After HTTP header sent with
70
+ 429 Too Many Requests or 503 Service Unavailable responses.
71
+ (:issue:`307`)
72
+ * Optimize OR-only :meth:`~Reader.get_feeds()` and :meth:`~Reader.get_entries()`
73
+ tag filters (e.g. ``[['one', ...]]``). (:issue:`359`)
74
+ * Expose :data:`reader.core.DEFAULT_RESERVED_NAME_SCHEME`. (:issue:`287`)
75
+
76
+ * Update the (unstable) parser API to expose HTTP information to the updater.
77
+ (:issue:`307`)
78
+
79
+ * The :class:`.RetrieverType` protocol used by retrievers changed
80
+ (new return type, allow storing arbitrary caching data via
81
+ :attr:`~.RetrievedFeed.caching_info`).
82
+ * The ``HTTPAcceptParserType`` was renamed to :class:`.AcceptParserType`.
83
+
84
+ * Allow :ref:`using the installed feedparser <no-vendored-feedparser>`
85
+ instead of the one vendored by *reader*.
86
+ This is useful for working around issues in the vendored feedparser,
87
+ like the libxml2 <=2.13.3 incompatibility reported in :issue:`350`;
88
+ thanks to `Maks Verver`_ for reporting,
89
+ root causing, and following up with both dependencies.
90
+
91
+ * Fix a number of brittle / broken tests.
92
+ Thanks to `Maks Verver`_ for the issues and fixes.
93
+ (:issue:`348`, :issue:`349`, :issue:`355`)
94
+
95
+ .. _Maks Verver: https://github.com/maksverver
7
96
 
8
97
 
9
98
  Version 3.14
@@ -650,6 +739,8 @@ Released 2022-03-12
650
739
  Fix broken error flashing.
651
740
 
652
741
 
742
+ .. _version 2.9:
743
+
653
744
  Version 2.9
654
745
  -----------
655
746
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.14
3
+ Version: 3.16
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:
@@ -40,6 +40,9 @@ Data objects
40
40
  .. autoclass:: Enclosure
41
41
  :members:
42
42
 
43
+ .. autoclass:: EntrySource
44
+ :members:
45
+
43
46
  .. autoclass:: EntrySearchResult
44
47
  :members:
45
48
 
@@ -184,4 +187,13 @@ Type aliases
184
187
  Constants
185
188
  ---------
186
189
 
190
+ .. autodata:: reader.core.DEFAULT_RESERVED_NAME_SCHEME
191
+
187
192
  .. autodata:: reader.plugins.DEFAULT_PLUGINS
193
+
194
+
195
+
196
+ Utilities
197
+ ---------
198
+
199
+ .. autofunction:: reader.utils.archive_entries
@@ -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.
@@ -34,6 +34,9 @@ but I will prioritize supporting :doc:`contributors <contributing>`
34
34
 
35
35
  * OPML support, :issue:`165`
36
36
  * :ref:`deleting entries <deleting entries>`
37
+
38
+ * archiving important entries of deleted feeds, :issue:`290`
39
+
37
40
  * :ref:`feed interaction statistics <counts api>`
38
41
 
39
42
  * security
@@ -69,7 +72,6 @@ but I will prioritize supporting :doc:`contributors <contributing>`
69
72
 
70
73
  * add more fields to data objects
71
74
 
72
- * entry source, :issue:`276`
73
75
  * extra data, as an escape hatch, :issue:`277`
74
76
 
75
77
  * :ref:`multiple storage implementations <multiple storage implementations>`
@@ -378,8 +380,14 @@ Requests session plugins:
378
380
  Retriever / parser split:
379
381
 
380
382
  * :issue:`205#issuecomment-766321855`
381
- * split exception hierarchy (not implemented as of 3.9): :issue:`218#issuecomment-1687094315`
383
+ * split exception hierarchy (not implemented as of 3.15): :issue:`218#issuecomment-1687094315`
384
+ * API overview as of 3.14, meant to show the dataflow: :issue:`307#issuecomment-2266647310`
385
+ * ~ideal API (mostly implemented in 3.15): :issue:`307#issuecomment-2281797898`
382
386
 
387
+ * exposes HTTP information (so it can be used by the updater)
388
+ * introduces ~internal RetrieveError and NotModified :exc:`ParseError` subclasses;
389
+ notably, this doesn't really follow the split exception hierarchy mentioned above,
390
+ and is only meant to surface HTTP information in error cases
383
391
 
384
392
  Alternative feed parsers:
385
393
 
@@ -398,17 +406,13 @@ Lessons learned from the :ref:`Twitter` plugin:
398
406
  :meth:`~reader._parser.ParserType` being generic.
399
407
 
400
408
  * It is useful for a Retriever to store arbitrary caching data;
401
- the plugin (mis)used
402
- :attr:`~reader._parser.RetrieveResult.http_etag`
409
+ the plugin (mis)used ``RetrieveResult.http_etag``
403
410
  to store the (integer) id of the newest tweet in the thread.
404
411
 
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>`_.
412
+ Update: This was formalized as :attr:`.RetrievedFeed.caching_info` in 3.15.
408
413
 
409
414
  * 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
415
+ the plugin (mis)used ``FeedForUpdate.http_etag`` to pass from
412
416
  :meth:`~reader._parser.FeedForUpdateRetrieverType.process_feed_for_update`
413
417
  to :meth:`~reader._parser.RetrieverType.__call__`:
414
418
 
@@ -510,6 +514,16 @@ From the initial issue:
510
514
  * use cases: :issue:`149#issuecomment-700066794`
511
515
  * initial requirements: :issue:`149#issuecomment-700532183`
512
516
 
517
+ Splitting a feed into two feeds is discussed in :issue:`221`.
518
+ The main takeaway at the time (2021) was that
519
+ not all entry data can be retrieved through the storage API,
520
+ so it's not fully possible to round-trip an entry.
521
+ Based on a quick 2024 survey,
522
+ the only affected attributes
523
+ are :attr:`~.EntryUpdateIntent.feed_order` (can't get; not critical)
524
+ and :attr:`~Entry.original_feed_url` (can't set; will be required for
525
+ archiving important entries of deleted feeds, :issue:`290`).
526
+
513
527
 
514
528
  Resource tags / metadata
515
529
  ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -684,6 +698,14 @@ Update hook error handling:
684
698
  * expected behavior: :issue:`218#issuecomment-1595691410`, :issue:`218#issuecomment-1666823222`
685
699
  * update hook pseudocode + exception hierarchy: :issue:`218#issuecomment-1666869215`
686
700
 
701
+ Considerations on using pluggy: :issue:`329`,
702
+ `how adapting reader use cases to the pluggy model may work <https://github.com/pytest-dev/pluggy/issues/151#issuecomment-2421648901>`_.
703
+ Conclusions:
704
+
705
+ even if using pluggy would require some work-arounds for calling, I think the collection / discovery functionality is still more than worth it
706
+
707
+ long-term and/or if reader becomes really popular, pluggy is definitely the way to go
708
+
687
709
 
688
710
  Reserved names
689
711
  ~~~~~~~~~~~~~~
@@ -698,16 +720,16 @@ Wrapping underlying storage exceptions
698
720
 
699
721
  Which exception to wrap, and which not: :issue:`21#issuecomment-365442439`.
700
722
 
701
- In version 3.10 (November 2023), all internal APIs were changed
702
- to use timezone-aware datetimes, with the timezone set to UTC,
703
- in preparation for support for any timezone.
704
-
705
723
 
706
724
  Timezone handling
707
725
  ~~~~~~~~~~~~~~~~~
708
726
 
709
727
  Aware vs. naive, and what's needed to go fully aware: :issue:`233#issuecomment-881618002`.
710
728
 
729
+ In version 3.10 (November 2023), all internal APIs were changed
730
+ to use timezone-aware datetimes, with the timezone set to UTC,
731
+ in preparation for support for any timezone.
732
+
711
733
 
712
734
  OPML support
713
735
  ~~~~~~~~~~~~
@@ -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.16'
41
41
 
42
42
  # isort: off
43
43
 
@@ -52,6 +52,7 @@ from .types import (
52
52
  Entry as Entry,
53
53
  Content as Content,
54
54
  Enclosure as Enclosure,
55
+ EntrySource as EntrySource,
55
56
  EntrySearchResult as EntrySearchResult,
56
57
  HighlightedString as HighlightedString,
57
58
  FeedCounts as FeedCounts,