reader 3.17__tar.gz → 3.18__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 (237) hide show
  1. {reader-3.17 → reader-3.18}/CHANGES.rst +19 -0
  2. {reader-3.17/src/reader.egg-info → reader-3.18}/PKG-INFO +4 -4
  3. {reader-3.17 → reader-3.18}/docs/api.rst +17 -0
  4. {reader-3.17 → reader-3.18}/pyproject.toml +2 -3
  5. {reader-3.17 → reader-3.18}/src/reader/__init__.py +4 -1
  6. {reader-3.17 → reader-3.18}/src/reader/_parser/__init__.py +2 -7
  7. {reader-3.17 → reader-3.18}/src/reader/_parser/_lazy.py +1 -2
  8. {reader-3.17 → reader-3.18}/src/reader/_parser/requests/_lazy.py +1 -2
  9. {reader-3.17 → reader-3.18}/src/reader/_plugins/cli_status.py +2 -2
  10. {reader-3.17 → reader-3.18}/src/reader/_storage/_base.py +1 -1
  11. {reader-3.17 → reader-3.18}/src/reader/_storage/_entries.py +6 -6
  12. {reader-3.17 → reader-3.18}/src/reader/_storage/_feeds.py +6 -4
  13. {reader-3.17 → reader-3.18}/src/reader/_storage/_search.py +12 -11
  14. {reader-3.17 → reader-3.18}/src/reader/_storage/_sql_utils.py +2 -3
  15. {reader-3.17 → reader-3.18}/src/reader/_storage/_sqlite_utils.py +11 -2
  16. {reader-3.17 → reader-3.18}/src/reader/_types.py +3 -7
  17. {reader-3.17 → reader-3.18}/src/reader/_update.py +1 -2
  18. {reader-3.17 → reader-3.18}/src/reader/core.py +39 -87
  19. {reader-3.17 → reader-3.18}/src/reader/exceptions.py +15 -81
  20. {reader-3.17 → reader-3.18}/src/reader/types.py +74 -22
  21. {reader-3.17 → reader-3.18/src/reader.egg-info}/PKG-INFO +4 -4
  22. {reader-3.17 → reader-3.18}/tests/data/full.atom.py +5 -11
  23. {reader-3.17 → reader-3.18}/tests/data/full.json.py +3 -3
  24. {reader-3.17 → reader-3.18}/tests/data/full.rss.py +3 -3
  25. {reader-3.17 → reader-3.18}/tests/test_plugins_cli_status.py +1 -0
  26. {reader-3.17 → reader-3.18}/tests/test_reader.py +5 -0
  27. {reader-3.17 → reader-3.18}/tests/test_reader_hooks.py +14 -4
  28. {reader-3.17 → reader-3.18}/tests/test_reader_update.py +17 -0
  29. {reader-3.17 → reader-3.18}/tox.ini +3 -3
  30. {reader-3.17 → reader-3.18}/LICENSE +0 -0
  31. {reader-3.17 → reader-3.18}/MANIFEST.in +0 -0
  32. {reader-3.17 → reader-3.18}/README.rst +0 -0
  33. {reader-3.17 → reader-3.18}/docs/.DS_Store +0 -0
  34. {reader-3.17 → reader-3.18}/docs/Makefile +0 -0
  35. {reader-3.17 → reader-3.18}/docs/app.rst +0 -0
  36. {reader-3.17 → reader-3.18}/docs/changelog.rst +0 -0
  37. {reader-3.17 → reader-3.18}/docs/cli.rst +0 -0
  38. {reader-3.17 → reader-3.18}/docs/conf.py +0 -0
  39. {reader-3.17 → reader-3.18}/docs/config.rst +0 -0
  40. {reader-3.17 → reader-3.18}/docs/contributing.rst +0 -0
  41. {reader-3.17 → reader-3.18}/docs/dev-app.rst +0 -0
  42. {reader-3.17 → reader-3.18}/docs/dev.rst +0 -0
  43. {reader-3.17 → reader-3.18}/docs/guide.rst +0 -0
  44. {reader-3.17 → reader-3.18}/docs/images/redesign-01.png +0 -0
  45. {reader-3.17 → reader-3.18}/docs/index.rst +0 -0
  46. {reader-3.17 → reader-3.18}/docs/install.rst +0 -0
  47. {reader-3.17 → reader-3.18}/docs/internal.rst +0 -0
  48. {reader-3.17 → reader-3.18}/docs/make.bat +0 -0
  49. {reader-3.17 → reader-3.18}/docs/plugins.rst +0 -0
  50. {reader-3.17 → reader-3.18}/docs/screenshots/dillo.png +0 -0
  51. {reader-3.17 → reader-3.18}/docs/screenshots/entries-feed.png +0 -0
  52. {reader-3.17 → reader-3.18}/docs/screenshots/entries-v2-dark.png +0 -0
  53. {reader-3.17 → reader-3.18}/docs/screenshots/entries-v2-filters-light.png +0 -0
  54. {reader-3.17 → reader-3.18}/docs/screenshots/entries.png +0 -0
  55. {reader-3.17 → reader-3.18}/docs/screenshots/entry-one.png +0 -0
  56. {reader-3.17 → reader-3.18}/docs/screenshots/entry-two.png +0 -0
  57. {reader-3.17 → reader-3.18}/docs/screenshots/feeds.png +0 -0
  58. {reader-3.17 → reader-3.18}/docs/screenshots/lynx.png +0 -0
  59. {reader-3.17 → reader-3.18}/docs/screenshots/search.png +0 -0
  60. {reader-3.17 → reader-3.18}/docs/tutorial.rst +0 -0
  61. {reader-3.17 → reader-3.18}/docs/why.rst +0 -0
  62. {reader-3.17 → reader-3.18}/examples/config.yaml +0 -0
  63. {reader-3.17 → reader-3.18}/examples/custom_headers.py +0 -0
  64. {reader-3.17 → reader-3.18}/examples/feed_slugs.py +0 -0
  65. {reader-3.17 → reader-3.18}/examples/parser_only.py +0 -0
  66. {reader-3.17 → reader-3.18}/examples/podcast.py +0 -0
  67. {reader-3.17 → reader-3.18}/examples/terminal.py +0 -0
  68. {reader-3.17 → reader-3.18}/run.sh +0 -0
  69. {reader-3.17 → reader-3.18}/scripts/.coverage +0 -0
  70. {reader-3.17 → reader-3.18}/scripts/backup.sh +0 -0
  71. {reader-3.17 → reader-3.18}/scripts/bench.py +0 -0
  72. {reader-3.17 → reader-3.18}/scripts/debug_storage_stats.py +0 -0
  73. {reader-3.17 → reader-3.18}/scripts/generate_import_all.py +0 -0
  74. {reader-3.17 → reader-3.18}/scripts/jscontrols.html +0 -0
  75. {reader-3.17 → reader-3.18}/scripts/jscontrols.py +0 -0
  76. {reader-3.17 → reader-3.18}/scripts/lines.sh +0 -0
  77. {reader-3.17 → reader-3.18}/scripts/release.py +0 -0
  78. {reader-3.17 → reader-3.18}/setup.cfg +0 -0
  79. {reader-3.17 → reader-3.18}/src/reader/__main__.py +0 -0
  80. {reader-3.17 → reader-3.18}/src/reader/_app/__init__.py +0 -0
  81. {reader-3.17 → reader-3.18}/src/reader/_app/api_thing.py +0 -0
  82. {reader-3.17 → reader-3.18}/src/reader/_app/cli.py +0 -0
  83. {reader-3.17 → reader-3.18}/src/reader/_app/static/controls.js +0 -0
  84. {reader-3.17 → reader-3.18}/src/reader/_app/static/style.css +0 -0
  85. {reader-3.17 → reader-3.18}/src/reader/_app/templates/add_entry.html +0 -0
  86. {reader-3.17 → reader-3.18}/src/reader/_app/templates/entries.html +0 -0
  87. {reader-3.17 → reader-3.18}/src/reader/_app/templates/entry.html +0 -0
  88. {reader-3.17 → reader-3.18}/src/reader/_app/templates/feeds.html +0 -0
  89. {reader-3.17 → reader-3.18}/src/reader/_app/templates/layout.html +0 -0
  90. {reader-3.17 → reader-3.18}/src/reader/_app/templates/macros.html +0 -0
  91. {reader-3.17 → reader-3.18}/src/reader/_app/templates/metadata.html +0 -0
  92. {reader-3.17 → reader-3.18}/src/reader/_app/templates/tags.html +0 -0
  93. {reader-3.17 → reader-3.18}/src/reader/_app/v2/__init__.py +0 -0
  94. {reader-3.17 → reader-3.18}/src/reader/_app/v2/forms.py +0 -0
  95. {reader-3.17 → reader-3.18}/src/reader/_app/v2/static/style.css +0 -0
  96. {reader-3.17 → reader-3.18}/src/reader/_app/v2/static/theme.js +0 -0
  97. {reader-3.17 → reader-3.18}/src/reader/_app/v2/templates/v2/entries.html +0 -0
  98. {reader-3.17 → reader-3.18}/src/reader/_app/v2/templates/v2/layout.html +0 -0
  99. {reader-3.17 → reader-3.18}/src/reader/_app/v2/templates/v2/macros.html +0 -0
  100. {reader-3.17 → reader-3.18}/src/reader/_app/wsgi.py +0 -0
  101. {reader-3.17 → reader-3.18}/src/reader/_cli.py +0 -0
  102. {reader-3.17 → reader-3.18}/src/reader/_config.py +0 -0
  103. {reader-3.17 → reader-3.18}/src/reader/_hash_utils.py +0 -0
  104. {reader-3.17 → reader-3.18}/src/reader/_parser/_http_utils.py +0 -0
  105. {reader-3.17 → reader-3.18}/src/reader/_parser/_url_utils.py +0 -0
  106. {reader-3.17 → reader-3.18}/src/reader/_parser/feedparser.py +0 -0
  107. {reader-3.17 → reader-3.18}/src/reader/_parser/file.py +0 -0
  108. {reader-3.17 → reader-3.18}/src/reader/_parser/http.py +0 -0
  109. {reader-3.17 → reader-3.18}/src/reader/_parser/jsonfeed.py +0 -0
  110. {reader-3.17 → reader-3.18}/src/reader/_parser/requests/__init__.py +0 -0
  111. {reader-3.17 → reader-3.18}/src/reader/_plugins/__init__.py +0 -0
  112. {reader-3.17 → reader-3.18}/src/reader/_plugins/enclosure_tags.py +0 -0
  113. {reader-3.17 → reader-3.18}/src/reader/_plugins/preview_feed_list.py +0 -0
  114. {reader-3.17 → reader-3.18}/src/reader/_plugins/share.py +0 -0
  115. {reader-3.17 → reader-3.18}/src/reader/_plugins/sqlite_releases.py +0 -0
  116. {reader-3.17 → reader-3.18}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  117. {reader-3.17 → reader-3.18}/src/reader/_plugins/timer.py +0 -0
  118. {reader-3.17 → reader-3.18}/src/reader/_storage/__init__.py +0 -0
  119. {reader-3.17 → reader-3.18}/src/reader/_storage/_changes.py +0 -0
  120. {reader-3.17 → reader-3.18}/src/reader/_storage/_html_utils.py +0 -0
  121. {reader-3.17 → reader-3.18}/src/reader/_storage/_schema.py +0 -0
  122. {reader-3.17 → reader-3.18}/src/reader/_storage/_tags.py +0 -0
  123. {reader-3.17 → reader-3.18}/src/reader/_utils.py +0 -0
  124. {reader-3.17 → reader-3.18}/src/reader/_vendor/__init__.py +0 -0
  125. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/__init__.py +0 -0
  126. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/api.py +0 -0
  127. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
  128. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
  129. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
  130. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
  131. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
  132. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
  133. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
  134. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
  135. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
  136. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/encodings.py +0 -0
  137. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/exceptions.py +0 -0
  138. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/html.py +0 -0
  139. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/http.py +0 -0
  140. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/mixin.py +0 -0
  141. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  142. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
  143. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
  144. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
  145. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
  146. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
  147. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
  148. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
  149. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
  150. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  151. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
  152. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
  153. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
  154. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/py.typed +0 -0
  155. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
  156. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/sgml.py +0 -0
  157. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/urls.py +0 -0
  158. {reader-3.17 → reader-3.18}/src/reader/_vendor/feedparser/util.py +0 -0
  159. {reader-3.17 → reader-3.18}/src/reader/plugins/__init__.py +0 -0
  160. {reader-3.17 → reader-3.18}/src/reader/plugins/enclosure_dedupe.py +0 -0
  161. {reader-3.17 → reader-3.18}/src/reader/plugins/entry_dedupe.py +0 -0
  162. {reader-3.17 → reader-3.18}/src/reader/plugins/mark_as_read.py +0 -0
  163. {reader-3.17 → reader-3.18}/src/reader/plugins/readtime.py +0 -0
  164. {reader-3.17 → reader-3.18}/src/reader/plugins/ua_fallback.py +0 -0
  165. {reader-3.17 → reader-3.18}/src/reader/py.typed +0 -0
  166. {reader-3.17 → reader-3.18}/src/reader/utils.py +0 -0
  167. {reader-3.17 → reader-3.18}/src/reader.egg-info/SOURCES.txt +0 -0
  168. {reader-3.17 → reader-3.18}/src/reader.egg-info/dependency_links.txt +0 -0
  169. {reader-3.17 → reader-3.18}/src/reader.egg-info/requires.txt +0 -0
  170. {reader-3.17 → reader-3.18}/src/reader.egg-info/top_level.txt +0 -0
  171. {reader-3.17 → reader-3.18}/tests/conftest.py +0 -0
  172. {reader-3.17 → reader-3.18}/tests/data/10.json +0 -0
  173. {reader-3.17 → reader-3.18}/tests/data/10.json.py +0 -0
  174. {reader-3.17 → reader-3.18}/tests/data/custom +0 -0
  175. {reader-3.17 → reader-3.18}/tests/data/empty.atom +0 -0
  176. {reader-3.17 → reader-3.18}/tests/data/empty.atom.py +0 -0
  177. {reader-3.17 → reader-3.18}/tests/data/empty.json +0 -0
  178. {reader-3.17 → reader-3.18}/tests/data/empty.json.py +0 -0
  179. {reader-3.17 → reader-3.18}/tests/data/empty.rss +0 -0
  180. {reader-3.17 → reader-3.18}/tests/data/empty.rss.py +0 -0
  181. {reader-3.17 → reader-3.18}/tests/data/full.atom +0 -0
  182. {reader-3.17 → reader-3.18}/tests/data/full.json +0 -0
  183. {reader-3.17 → reader-3.18}/tests/data/full.rss +0 -0
  184. {reader-3.17 → reader-3.18}/tests/data/invalid.json +0 -0
  185. {reader-3.17 → reader-3.18}/tests/data/invalid.json.py +0 -0
  186. {reader-3.17 → reader-3.18}/tests/data/relative.atom +0 -0
  187. {reader-3.17 → reader-3.18}/tests/data/relative.atom.py +0 -0
  188. {reader-3.17 → reader-3.18}/tests/data/relative.rss +0 -0
  189. {reader-3.17 → reader-3.18}/tests/data/relative.rss.py +0 -0
  190. {reader-3.17 → reader-3.18}/tests/data/sqlite_releases.html +0 -0
  191. {reader-3.17 → reader-3.18}/tests/data/unknown.json +0 -0
  192. {reader-3.17 → reader-3.18}/tests/data/unknown.json.py +0 -0
  193. {reader-3.17 → reader-3.18}/tests/fakeparser.py +0 -0
  194. {reader-3.17 → reader-3.18}/tests/reader_methods.py +0 -0
  195. {reader-3.17 → reader-3.18}/tests/reader_test_plugins/__init__.py +0 -0
  196. {reader-3.17 → reader-3.18}/tests/reader_test_plugins/good.py +0 -0
  197. {reader-3.17 → reader-3.18}/tests/reader_test_plugins/init_error.py +0 -0
  198. {reader-3.17 → reader-3.18}/tests/reader_test_plugins/missing_dependency.py +0 -0
  199. {reader-3.17 → reader-3.18}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  200. {reader-3.17 → reader-3.18}/tests/test__types.py +0 -0
  201. {reader-3.17 → reader-3.18}/tests/test__utils.py +0 -0
  202. {reader-3.17 → reader-3.18}/tests/test_app.py +0 -0
  203. {reader-3.17 → reader-3.18}/tests/test_app_wsgi.py +0 -0
  204. {reader-3.17 → reader-3.18}/tests/test_bench.py +0 -0
  205. {reader-3.17 → reader-3.18}/tests/test_changes.py +0 -0
  206. {reader-3.17 → reader-3.18}/tests/test_cli.py +0 -0
  207. {reader-3.17 → reader-3.18}/tests/test_config.py +0 -0
  208. {reader-3.17 → reader-3.18}/tests/test_exceptions.py +0 -0
  209. {reader-3.17 → reader-3.18}/tests/test_hash_utils.py +0 -0
  210. {reader-3.17 → reader-3.18}/tests/test_html_utils.py +0 -0
  211. {reader-3.17 → reader-3.18}/tests/test_lazy_imports.py +0 -0
  212. {reader-3.17 → reader-3.18}/tests/test_parser.py +0 -0
  213. {reader-3.17 → reader-3.18}/tests/test_plugins_enclosure_dedupe.py +0 -0
  214. {reader-3.17 → reader-3.18}/tests/test_plugins_entry_dedupe.py +0 -0
  215. {reader-3.17 → reader-3.18}/tests/test_plugins_mark_as_read.py +0 -0
  216. {reader-3.17 → reader-3.18}/tests/test_plugins_preview_feed_list.py +0 -0
  217. {reader-3.17 → reader-3.18}/tests/test_plugins_readtime.py +0 -0
  218. {reader-3.17 → reader-3.18}/tests/test_plugins_sqlite_releases.py +0 -0
  219. {reader-3.17 → reader-3.18}/tests/test_plugins_ua_fallback.py +0 -0
  220. {reader-3.17 → reader-3.18}/tests/test_reader_context.py +0 -0
  221. {reader-3.17 → reader-3.18}/tests/test_reader_counts.py +0 -0
  222. {reader-3.17 → reader-3.18}/tests/test_reader_deprecations.py +0 -0
  223. {reader-3.17 → reader-3.18}/tests/test_reader_filter.py +0 -0
  224. {reader-3.17 → reader-3.18}/tests/test_reader_integration.py +0 -0
  225. {reader-3.17 → reader-3.18}/tests/test_reader_plugins.py +0 -0
  226. {reader-3.17 → reader-3.18}/tests/test_reader_private.py +0 -0
  227. {reader-3.17 → reader-3.18}/tests/test_reader_search.py +0 -0
  228. {reader-3.17 → reader-3.18}/tests/test_reader_sort.py +0 -0
  229. {reader-3.17 → reader-3.18}/tests/test_reader_utils.py +0 -0
  230. {reader-3.17 → reader-3.18}/tests/test_search.py +0 -0
  231. {reader-3.17 → reader-3.18}/tests/test_sql_utils.py +0 -0
  232. {reader-3.17 → reader-3.18}/tests/test_sqlite_utils.py +0 -0
  233. {reader-3.17 → reader-3.18}/tests/test_storage.py +0 -0
  234. {reader-3.17 → reader-3.18}/tests/test_tags.py +0 -0
  235. {reader-3.17 → reader-3.18}/tests/test_test_utils.py +0 -0
  236. {reader-3.17 → reader-3.18}/tests/test_types.py +0 -0
  237. {reader-3.17 → reader-3.18}/tests/utils.py +0 -0
@@ -6,6 +6,25 @@ Changelog
6
6
  :no-index:
7
7
 
8
8
 
9
+ Version 3.18
10
+ ------------
11
+
12
+ Released 2025-05-04
13
+
14
+ * Drop Python 3.10 support. (:issue:`354`)
15
+
16
+ * Add the ``overwrite`` argument to :meth:`~Reader.add_entry`.
17
+ * Add :class:`FeedSort`, :class:`EntrySort`, and :class:`EntrySearchSort` enums.
18
+ (:issue:`354`)
19
+
20
+ * Fix bug causing :meth:`.Reader.update_feeds`
21
+ to not honor ``updates_enabled=True`` when ``scheduled=True``.
22
+ (:issue:`365`)
23
+
24
+ * In the :mod:`~reader._plugins.cli_status` plugin,
25
+ overwrite the entry so it doesn't appear briefly as unread.
26
+
27
+
9
28
  Version 3.17
10
29
  ------------
11
30
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: reader
3
- Version: 3.17
3
+ Version: 3.18
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -19,7 +19,6 @@ Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Python :: 3
21
21
  Classifier: Programming Language :: Python :: 3 :: Only
22
- Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
23
  Classifier: Programming Language :: Python :: 3.12
25
24
  Classifier: Programming Language :: Python :: 3.13
@@ -32,7 +31,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
32
31
  Classifier: Topic :: Software Development :: Libraries
33
32
  Classifier: Topic :: Utilities
34
33
  Classifier: Typing :: Typed
35
- Requires-Python: >=3.10
34
+ Requires-Python: >=3.11
36
35
  Description-Content-Type: text/x-rst
37
36
  License-File: LICENSE
38
37
  Requires-Dist: typing-extensions>=4
@@ -90,6 +89,7 @@ Requires-Dist: tox; extra == "dev"
90
89
  Requires-Dist: pre-commit; extra == "dev"
91
90
  Requires-Dist: build; extra == "dev"
92
91
  Requires-Dist: twine; extra == "dev"
92
+ Dynamic: license-file
93
93
 
94
94
  .. begin-intro
95
95
 
@@ -171,6 +171,23 @@ The class hierarchy for :mod:`reader` exceptions is:
171
171
 
172
172
 
173
173
 
174
+ Enumerations
175
+ ------------
176
+
177
+ .. autoclass:: reader.FeedSort(value)
178
+ :show-inheritance:
179
+ :members:
180
+
181
+ .. autoclass:: reader.EntrySort(value)
182
+ :show-inheritance:
183
+ :members:
184
+
185
+ .. autoclass:: reader.EntrySearchSort(value)
186
+ :show-inheritance:
187
+ :members:
188
+
189
+
190
+
174
191
  .. _type aliases:
175
192
 
176
193
  Type aliases
@@ -23,7 +23,6 @@ classifiers = [
23
23
  "Programming Language :: Python",
24
24
  "Programming Language :: Python :: 3",
25
25
  "Programming Language :: Python :: 3 :: Only",
26
- "Programming Language :: Python :: 3.10",
27
26
  "Programming Language :: Python :: 3.11",
28
27
  "Programming Language :: Python :: 3.12",
29
28
  "Programming Language :: Python :: 3.13",
@@ -37,7 +36,7 @@ classifiers = [
37
36
  "Topic :: Utilities",
38
37
  "Typing :: Typed",
39
38
  ]
40
- requires-python = ">=3.10"
39
+ requires-python = ">=3.11"
41
40
  dependencies = [
42
41
  "typing-extensions>=4",
43
42
  "feedparser>=6",
@@ -189,7 +188,7 @@ ignore_errors = true
189
188
 
190
189
  [tool.isort]
191
190
  profile = "black"
192
- py_version = 310
191
+ py_version = 311
193
192
  src_paths = ["src", "tests"]
194
193
  extend_skip = ["examples", "src/reader/_vendor"]
195
194
  skip_gitignore = true
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
37
37
 
38
38
  """
39
39
 
40
- __version__ = '3.17'
40
+ __version__ = '3.18'
41
41
 
42
42
  # isort: off
43
43
 
@@ -58,6 +58,9 @@ from .types import (
58
58
  FeedCounts as FeedCounts,
59
59
  EntryCounts as EntryCounts,
60
60
  EntrySearchCounts as EntrySearchCounts,
61
+ FeedSort as FeedSort,
62
+ EntrySort as EntrySort,
63
+ EntrySearchSort as EntrySearchSort,
61
64
  UpdateResult as UpdateResult,
62
65
  UpdatedFeed as UpdatedFeed,
63
66
  EntryUpdateStatus as EntryUpdateStatus,
@@ -239,13 +239,9 @@ class NotModified(RetrieveError):
239
239
  _default_message = "not modified"
240
240
 
241
241
 
242
- @dataclass(frozen=True)
243
- class RetrieveResult(_namedtuple_compat, Generic[F, T, E]):
242
+ class RetrieveResult(NamedTuple, Generic[F, T, E]):
244
243
  """The result of retrieving a feed, regardless of the outcome."""
245
244
 
246
- # should be a NamedTuple, but the typing one became generic only in 3.11,
247
- # and we don't want to depend on typing_extensions at runtime
248
-
249
245
  #: The feed (a :class:`FeedArgument`, usually a :class:`FeedForUpdate`).
250
246
  feed: F
251
247
 
@@ -339,8 +335,7 @@ class FeedForUpdateRetrieverType(RetrieverType[T_co], Protocol): # pragma: no c
339
335
  """
340
336
 
341
337
 
342
- @dataclass(frozen=True)
343
- class ParseResult(_namedtuple_compat, Generic[F, E]):
338
+ class ParseResult(NamedTuple, Generic[F, E]):
344
339
  """The result of retrieving and parsing a feed, regardless of the outcome."""
345
340
 
346
341
  #: The feed (a :class:`FeedArgument`, usually a :class:`.FeedForUpdate`).
@@ -259,8 +259,7 @@ class Parser:
259
259
  Takes one argument and does not raise exceptions.
260
260
 
261
261
  """
262
- feed = result.feed
263
- context = result.value
262
+ feed, context = result
264
263
 
265
264
  http_info = None
266
265
  value: ParsedFeed | None | Exception
@@ -4,6 +4,7 @@ from collections.abc import Sequence
4
4
  from dataclasses import dataclass
5
5
  from dataclasses import field
6
6
  from typing import Any
7
+ from typing import Self
7
8
  from typing import TYPE_CHECKING
8
9
  from typing import TypeVar
9
10
 
@@ -14,8 +15,6 @@ from . import TimeoutType
14
15
 
15
16
 
16
17
  if TYPE_CHECKING: # pragma: no cover
17
- from typing_extensions import Self
18
-
19
18
  from . import Headers
20
19
  from . import RequestHook
21
20
  from . import ResponseHook
@@ -73,7 +73,6 @@ def save_output(reader, now, config, command_path, output):
73
73
 
74
74
  try:
75
75
  old_content = reader.get_entry((feed_url, entry_id)).content[0].value
76
- reader.delete_entry((feed_url, entry_id))
77
76
  except EntryNotFoundError:
78
77
  old_content = ''
79
78
 
@@ -91,7 +90,8 @@ def save_output(reader, now, config, command_path, output):
91
90
  title=title,
92
91
  updated=now,
93
92
  content=[dict(type='text/plain', value=''.join(parts))],
94
- )
93
+ ),
94
+ overwrite=True,
95
95
  )
96
96
  reader.mark_entry_as_read((feed_url, entry_id))
97
97
 
@@ -41,7 +41,7 @@ if debug: # pragma: no cover
41
41
  stmt = data.pop('stmt', None)
42
42
  print('STORAGE_DEBUG', json.dumps(data), file=sys.stderr)
43
43
  if stmt:
44
- print(stmt)
44
+ print(stmt, file=sys.stderr)
45
45
 
46
46
 
47
47
  wrap_exceptions = partial(_sqlite_utils.wrap_exceptions, StorageError)
@@ -55,7 +55,7 @@ class EntriesMixin(StorageBase):
55
55
  def get_entries(
56
56
  self,
57
57
  filter: EntryFilter = EntryFilter(), # noqa: B008
58
- sort: EntrySort = 'recent',
58
+ sort: EntrySort = EntrySort.RECENT,
59
59
  limit: int | None = None,
60
60
  starting_after: tuple[str, str] | None = None,
61
61
  ) -> Iterable[Entry]:
@@ -64,7 +64,7 @@ class EntriesMixin(StorageBase):
64
64
  partial(get_entries_query, filter, sort),
65
65
  row_factory=entry_factory,
66
66
  )
67
- if sort != 'random':
67
+ if sort != EntrySort.RANDOM:
68
68
  last = self.get_entry_last(sort, starting_after) if starting_after else None
69
69
  return paginated_query(limit, last)
70
70
  else:
@@ -571,7 +571,7 @@ RECENT_SORT_KEY = SortKey(
571
571
  desc=True,
572
572
  )
573
573
 
574
- ENTRY_SORT_KEYS = {'recent': RECENT_SORT_KEY}
574
+ ENTRY_SORT_KEYS = {EntrySort.RECENT: RECENT_SORT_KEY}
575
575
 
576
576
 
577
577
  def entries_recent_sort(
@@ -597,9 +597,9 @@ def entries_random_sort(query: Query) -> None:
597
597
  query.ORDER_BY("random()")
598
598
 
599
599
 
600
- ENTRIES_SORT: dict[str, Callable[[Query], None]] = {
601
- 'recent': entries_recent_sort,
602
- 'random': entries_random_sort,
600
+ ENTRIES_SORT: dict[EntrySort, Callable[[Query], None]] = {
601
+ EntrySort.RECENT: entries_recent_sort,
602
+ EntrySort.RANDOM: entries_random_sort,
603
603
  }
604
604
 
605
605
 
@@ -107,7 +107,7 @@ class FeedsMixin(StorageBase):
107
107
  def get_feeds(
108
108
  self,
109
109
  filter: FeedFilter = FeedFilter(), # noqa: B008
110
- sort: FeedSort = 'title',
110
+ sort: FeedSort = FeedSort.TITLE,
111
111
  limit: int | None = None,
112
112
  starting_after: str | None = None,
113
113
  ) -> Iterable[Feed]:
@@ -335,8 +335,10 @@ def feed_factory(row: tuple[Any, ...]) -> Feed:
335
335
  FEED_SORT_KEYS = {
336
336
  # values must be non-null, see #203 for details.
337
337
  # url at the end makes the order deterministic.
338
- 'title': SortKey(("kinda_title", "lower(coalesce(user_title, title, ''))"), "url"),
339
- 'added': SortKey("added", "url", desc=True),
338
+ FeedSort.TITLE: SortKey(
339
+ ("kinda_title", "lower(coalesce(user_title, title, ''))"), "url"
340
+ ),
341
+ FeedSort.ADDED: SortKey("added", "url", desc=True),
340
342
  }
341
343
 
342
344
 
@@ -358,7 +360,7 @@ def feed_filter(query: Query, filter: FeedFilter) -> dict[str, Any]:
358
360
  if new is not None:
359
361
  query.WHERE(f"last_retrieved is {'' if new else 'NOT'} NULL")
360
362
  if update_after is not None:
361
- query.WHERE("update_after is NULL or update_after <= :update_after")
363
+ query.WHERE("(update_after is NULL or update_after <= :update_after)")
362
364
  context.update(update_after=adapt_datetime(update_after))
363
365
 
364
366
  return context
@@ -28,8 +28,9 @@ from ..exceptions import SearchError
28
28
  from ..exceptions import SearchNotEnabledError
29
29
  from ..types import EntrySearchCounts
30
30
  from ..types import EntrySearchResult
31
+ from ..types import EntrySearchSort
32
+ from ..types import EntrySort
31
33
  from ..types import HighlightedString
32
- from ..types import SearchSortOrder
33
34
  from . import _entries
34
35
  from . import _html_utils
35
36
  from . import _sqlite_utils
@@ -425,7 +426,7 @@ class Search:
425
426
  self,
426
427
  query: str,
427
428
  filter: EntryFilter = EntryFilter(), # noqa: B008
428
- sort: SearchSortOrder = 'relevant',
429
+ sort: EntrySearchSort = EntrySearchSort.RELEVANT,
429
430
  limit: int | None = None,
430
431
  starting_after: tuple[str, str] | None = None,
431
432
  ) -> Iterable[EntrySearchResult]:
@@ -457,13 +458,13 @@ class Search:
457
458
  )
458
459
 
459
460
  # TODO: dupe of at least Storage.get_entries(), maybe deduplicate
460
- if sort != 'random':
461
+ if sort != EntrySearchSort.RANDOM:
461
462
  last = None
462
463
  if starting_after:
463
- if sort == 'relevant':
464
+ if sort == EntrySearchSort.RELEVANT:
464
465
  last = self.search_entry_last(query, starting_after)
465
- elif sort == 'recent':
466
- last = self.storage.get_entry_last(sort, starting_after)
466
+ elif sort == EntrySearchSort.RECENT:
467
+ last = self.storage.get_entry_last(EntrySort.RECENT, starting_after)
467
468
  else:
468
469
  assert False, "shouldn't get here" # noqa: B011; # pragma: no cover
469
470
 
@@ -527,7 +528,7 @@ class Search:
527
528
 
528
529
 
529
530
  def make_search_entries_query(
530
- filter: EntryFilter, sort: SearchSortOrder
531
+ filter: EntryFilter, sort: EntrySearchSort
531
532
  ) -> tuple[Query, dict[str, Any]]:
532
533
  search = (
533
534
  Query()
@@ -585,12 +586,12 @@ def relevant_sort(query: Query) -> None:
585
586
  )
586
587
 
587
588
 
588
- SEARCH_ENTRIES_SORT: dict[str, Callable[[Query], None]] = {
589
- 'relevant': relevant_sort,
590
- 'recent': partial(
589
+ SEARCH_ENTRIES_SORT: dict[EntrySearchSort, Callable[[Query], None]] = {
590
+ EntrySearchSort.RELEVANT: relevant_sort,
591
+ EntrySearchSort.RECENT: partial(
591
592
  _entries.entries_recent_sort, keyword='HAVING', id_prefix='search._'
592
593
  ),
593
- 'random': _entries.entries_random_sort,
594
+ EntrySearchSort.RANDOM: _entries.entries_random_sort,
594
595
  }
595
596
 
596
597
 
@@ -34,6 +34,7 @@ from collections.abc import Iterable
34
34
  from dataclasses import dataclass
35
35
  from typing import Any
36
36
  from typing import NamedTuple
37
+ from typing import Self
37
38
  from typing import TYPE_CHECKING
38
39
  from typing import TypeVar
39
40
  from typing import Union
@@ -42,8 +43,6 @@ from typing import Union
42
43
  if TYPE_CHECKING: # pragma: no cover
43
44
  import sqlite3
44
45
 
45
- from typing_extensions import Self
46
-
47
46
 
48
47
  _T = TypeVar('_T')
49
48
  _U = TypeVar('_U')
@@ -197,7 +196,7 @@ class BaseQuery:
197
196
 
198
197
  yield '\n'
199
198
 
200
- _indent = functools.partial(textwrap.indent, prefix=' ')
199
+ _indent = staticmethod(functools.partial(textwrap.indent, prefix=' '))
201
200
 
202
201
 
203
202
  if TYPE_CHECKING: # pragma: no cover
@@ -95,8 +95,17 @@ def wrap_exceptions(
95
95
  can only be fixed by the user (e.g. access permission denied), and aren't
96
96
  domain-related (those should have other custom exceptions).
97
97
 
98
- This is an imprecise science, since the DB-API exceptions are somewhat
99
- fuzzy in their meaning and we can't access the SQLite result code.
98
+ We intentionally rely on the error message, because:
99
+
100
+ * the DB-API exceptions are somewhat fuzzy in their meaning
101
+ * we can't access the SQLite result code prior to Python 3.11
102
+ * even having the primary result code (added in 3.11):
103
+ * it's generic SQLITE_ERROR for a lot of errors we care about
104
+ ("no such ...", "... already exists", invalid FTS5 queries)
105
+ * we need the table name so we don't accidentally shadow bugs
106
+ * we need the extended result code for the same reason
107
+ (e.g. SQLITE_CONSTRAINT vs SQLITE_CONSTRAINT_FOREIGNKEY)
108
+ * ProgrammingError usually does not have an error code
100
109
 
101
110
  Full discussion at https://github.com/lemon24/reader/issues/21
102
111
 
@@ -20,7 +20,7 @@ from typing import NamedTuple
20
20
  from typing import overload
21
21
  from typing import Protocol
22
22
  from typing import runtime_checkable
23
- from typing import TYPE_CHECKING
23
+ from typing import Self
24
24
  from typing import TypeVar
25
25
  from typing import Union
26
26
 
@@ -40,6 +40,7 @@ from .types import EntryCounts
40
40
  from .types import EntryInput
41
41
  from .types import EntrySearchCounts
42
42
  from .types import EntrySearchResult
43
+ from .types import EntrySearchSort
43
44
  from .types import EntrySort
44
45
  from .types import EntrySource
45
46
  from .types import ExceptionInfo
@@ -51,15 +52,10 @@ from .types import JSONType
51
52
  from .types import MISSING
52
53
  from .types import MissingType
53
54
  from .types import ResourceId
54
- from .types import SearchSortOrder
55
55
  from .types import TagFilterInput
56
56
  from .types import TristateFilterInput
57
57
 
58
58
 
59
- if TYPE_CHECKING: # pragma: no cover
60
- from typing_extensions import Self
61
-
62
-
63
59
  log = logging.getLogger("reader")
64
60
 
65
61
  # Private API
@@ -1268,7 +1264,7 @@ class SearchType(Protocol): # pragma: no cover
1268
1264
  query: str,
1269
1265
  /,
1270
1266
  filter: EntryFilter,
1271
- sort: SearchSortOrder,
1267
+ sort: EntrySearchSort,
1272
1268
  limit: int | None,
1273
1269
  starting_after: tuple[str, str] | None,
1274
1270
  ) -> Iterable[EntrySearchResult]:
@@ -430,8 +430,7 @@ class Pipeline:
430
430
  config: UpdateConfig,
431
431
  result: ParseResult[FeedForUpdate, ParseError],
432
432
  ) -> tuple[str, UpdatedFeed | None | Exception]:
433
- feed = result.feed
434
- value = result.value
433
+ feed, value, _ = result
435
434
 
436
435
  # TODO: don't duplicate code from update()
437
436
  # TODO: the feed tag value should come from get_feeds_for_update()