Version 3.12

Released 2024-03-05

  • Split the full-text search index into a separate, attached database. (#323)

  • Require at least SQLite 3.18. Previously, reader core required 3.15, and only update_search() required 3.18. (#323)

  • Enable write-ahead logging only once, when the database is created, instead of every time it is opened. (#323)

  • Vacuum the main database after migrations. (#323)

  • Add an internal change tracking API to formalize how search keeps in sync with storage. (#323)

  • Refactor storage internals. (#323)

Version 3.11

Released 2023-12-30

  • Allow filtering entries by their (entry) tags. (#328)

  • Support Python 3.12. (#326)

Version 3.10

Released 2023-11-12

  • Stop using deprecated sqlite3 datetime converters/adapters. (#321)

  • Document the storage Internal API. (#325)

  • Change all internal APIs to use timezone-aware datetimes, with the timezone set to UTC. (#321)

  • In the API documentation, fall back to type hints if hand-written parameter types are not available. Add relevant Documentation guidelines to the dev documentation. (#287)

  • Add the share experimental plugin to add social sharing links in the web app.

Version 3.9

Released 2023-08-28

Version 3.8

Released 2023-08-20

Version 3.7

Released 2023-07-15


This is the last release to support Python 3.9; see #302 for details.

  • Support PyPy 3.10. (#302)

  • Remove the twitter experimental plugin (deprecated in 3.6). (#310)

  • Remove the tumblr_gdpr experimental plugin (not needed since August 2020). (#315)

Version 3.6

Released 2023-06-16

  • Add documentation on How to contribute to reader and a detailed Roadmap. Thanks to Katharine Jarmul for finally getting me to do this. (#60)

  • Document the low-level delete_entries() storage method. (#301, #96)

  • Update vendored reader._http_utils to werkzeug 2.3.5.

  • Deprecate the twitter experimental plugin, since the Twitter API does not have a (useful) free tier anymore. (#310)


    The twitter plugin will be removed in version 3.7.

Version 3.5

Released 2023-03-19

  • Make Entry.important an optional boolean defaulting to None, so one can express “explicitly unimportant” (don’t care) by setting it to False. This replaces the semantics for don’t care introduced in version 2.2. (#254)


    This is a minor compatibility break, and should mostly affect code that checks identity (if entry.important is True: ...); code that uses important in a boolean context (if entry.important: ...) should not be affected.

  • set_entry_read() and set_entry_important() do not coerce the flag value to bool anymore, and require it to be True or False (or None).

Version 3.4

Released 2023-01-22

Version 3.3

Released 2022-12-19

This release marks reader’s 5th anniversary and its 2000th commit.


This is the last release to support Python 3.8; see #298 for details.

  • Support Python 3.11. (#289)

  • Postpone update-related imports until needed. Shortens time from process start to usable Reader instance by 3x (imports are 72% faster). (#297)

  • Refactor parser internals. (#297)


    Plugins using the (unstable) session hooks should replace:



  • twitter plugin: don’t fail when deserializing tweets with missing edit_history_tweet_ids (fails in tweepy 4.11, warns in tweepy >4.12).

Version 3.2

Released 2022-09-14

  • UpdatedFeed changes: added field unmodified and property total; fields new and modified became optional. (#96)

  • Fix bug in entry_dedupe causing updates to fail if there were multiple new duplicates of the same issue. (#292)

  • Fix bug in readtime and mark_as_read causing updates to fail if an entry was deleted by another plugin. (#292)

  • Fix bug in mark_as_read causing updates to fail if an entry had no title.

  • In the CLI, don’t suppress the traceback of ReaderError, since it would also suppress it for bugs.

  • In the CLI, stop using deprecated click.get_terminal_size().

Version 3.1

Released 2022-08-29

  • Drop readtime plugin dependency on readtime (which has a transitive dependency on lxml, which does not always have PyPy Windows wheels on PyPI). The readtime extra is deprecated, but remains available to avoid breaking dependent packages. (#286)

  • Sort entries by added date most of the time, with the exception of those imported on the first update. Previously, entries would be sorted by added only if they were published less than 7 days ago, causing entries that appear in the feed months after their published to never appear at the top (so the user would never see them). (#279)

Version 3.0

Released 2022-07-30


This release contains backwards incompatible changes.

Version 2.17

Released 2022-07-23

  • Deprecate the object_id property of data objects in favor of new property resource_id. resource_id is the same as object_id, except for feeds and feed-related exceptions it is of type tuple[str] instead of str. object_id will be removed in version 3.0. (#266, #268)

  • Do not attempt too hard to run PRAGMA optimize if the database is busy. Prevents rare “database is locked” errors when multiple threads using the same reader terminate at the same time. (#206)

Version 2.16

Released 2022-07-17

  • Allow using a Reader object from multiple threads directly (do not require it to be used as a context manager anymore). (#206)

  • Allow Reader objects to be reused after closing. (#206, #284)

  • Allow calling close() from any thread. (#206)

  • Allow using a Reader object from multiple asyncio tasks. (#206)

Version 2.15

Released 2022-07-08

  • Allow using Reader objects from threads other than the creating thread. (#206)

  • Allow using Reader objects as context managers. (#206)

Version 2.14

Released 2022-06-30

  • Mark reader as providing type information. Previously, code importing from reader would fail type checking with error: Skipping analyzing "reader": module is installed, but missing library stubs or py.typed marker. (#280)

  • Drop Python 3.7 support. (#278)

  • Support PyPy 3.9.

Version 2.13

Released 2022-06-28

  • Add the twitter experimental plugin, which allows using a Twitter account as a feed. (#271)

  • Skip with a warning entries that have no <guid> or <link> in an RSS feed; only raise ParseError if all entries have a missing id. (Note that both Atom and JSON Feed entries are required to have an id by their respective specifications.) Thanks to Mirek Długosz for the issue and pull request. (#281)

  • Add ReaderWarning.

Version 2.12

Released 2022-03-31

  • Add the readtime built-in plugin, which stores the entry read time as a tag during feed update. (#275)

  • Allow running arbitrary actions once before/after updating feeds via before_feeds_update_hooks / after_feeds_update_hooks.

  • Add Entry.get_content() and Content.is_html.

  • In the web app, use the read time provided by the readtime plugin, instead of calculating it on each page load. Speeds up the rendering of the entries page by 20-30%, hopefully winning back the time lost when the read time feature was first added in 2.6. (#275)

  • In the web app, also show the read time for search results.

Version 2.11

Released 2022-03-17

  • Fix issue causing make_reader() to fail with message database requirement error: required SQLite compile options missing: ['ENABLE_JSON1'] when using SQLite 3.38 or newer. (#273)

Version 2.10

Released 2022-03-12

  • Support entry and global tags. (#272, #228, #267)

  • Remove get_tags() support for the (None,) (any feed) and None (any resource) wildcard resource values.


    This is a minor compatibility break, but is unlikely to affect existing users; the usefulness of the wildcards was limited, because it was impossible to tell to which resource a (key, value) pair belongs.

  • Allow passing a (feed URL,) 1-tuple anywhere a feed URL can be passed to a Reader method.

  • Remove the global_metadata experimental plugin (superseded by global tags).

  • In the web application, support editing entry and global metadata. Fix broken delete metadata button. Fix broken error flashing.

Version 2.9

Released 2022-02-07

  • Decrease update_feeds() memory usage by ~35% (using the maxrss before the call as baseline; overall process maxrss decreases by ~20%). The improvement is not in reader code, but in feedparser; reader will temporarily vendor feedparser until the fix makes it upstream and is released on PyPI. (#265)

  • In the web application, allow sorting feeds by the number of entries: important, unread, per day during the last 1, 3, 12 months. (#249, #245).

Version 2.8

Released 2022-01-22

  • Add generic tag methods get_tags(), get_tag_keys(), get_tag(), set_tag(), and delete_tag(), providing a unified interface for accessing tags as key-value pairs. (#266)

    Add the TagError, TagNotFoundError, and ResourceNotFoundError exceptions.

  • Deprecate feed-specific tag and metadata methods (#266):

    Deprecate MetadataError, MetadataNotFoundError, and FeedMetadataNotFoundError.

    All deprecated methods/exceptions will be removed in version 3.0.

  • Add the missing_ok argument to delete_feed() and delete_entry().

  • Add the exist_ok argument to add_feed().

  • In the web application, show maxrss when debug is enabled. (#269)

  • In the web application, decrease memory usage of the entries page when there are a lot of entries (e.g. for 2.5k entries, maxrss decreased from 115 MiB to 75 MiB), at the expense of making “entries for feed” slightly slower. (#269)

Version 2.7

Released 2022-01-04

  • Tags and metadata now share the same namespace. See the Resource tags user guide section for details. (#266)

  • The mark_as_read plugin now uses the .reader.mark-as-read metadata for configuration. Feeds using the old metadata, .reader.mark_as_read, will be migrated automatically on update until reader 3.0.

  • Allow running arbitrary actions before updating feeds via before_feed_update_hooks.

  • Expose reader.plugins.DEFAULT_PLUGINS.

  • Add the global_metadata experimental plugin.

Version 2.6

Released 2021-11-15

  • Retrieve feeds in parallel, but parse them serially; previously, feeds would be parsed in parallel. Decreases Linux memory usage by ~20% when using workers; the macOS decrease is less notable. (#261)

  • Allow update_feeds() and update_feeds_iter() to filter feeds by feed, tags, broken, and updates_enabled. (#193, #219, #220)

  • Allow get_feeds() and get_feed_counts() to filter feeds by new. (#217)

  • Reuse the requests session when retrieving feeds; previously, each feed would get its own session.

  • Add support for CLI plugins.

  • Add the cli_status experimental plugin.

  • In the web application, show entry read time.

Version 2.5

Released 2021-10-28

Version 2.4

Released 2021-10-19

  • Enable search by default. (#252)

    • Add the search_enabled make_reader() argument. By default, search is enabled on the first update_search() call; the previous behavior was to do nothing.

    • Always install the full-text search dependencies (previously optional). The search extra remains available to avoid breaking dependent packages.

  • Add the subtitle and version Feed attributes. (#223)

  • Change the mark_as_read plugin to also explicitly mark matching entries as unimportant, similar to how the don’t care web application button works. (#260)

  • In the web application, show the feed subtitle. (#223)

Version 2.3

Released 2021-10-11

  • Support Python 3.10. (#248)

  • entry_dedupe now deletes old duplicates instead of marking them as read/unimportant. (#140)


    Please comment in #140 / open an issue if you were relying on the old behavior.

  • Fix entry_dedupe bug introduced in 2.2, causing the newest read entry to be marked as unread if none of its duplicates are read (idem for important). This was an issue only when re-running the plugin for existing entries, not for new entries (since new entries are unread/unimportant).

Version 2.2

Released 2021-10-08

Version 2.1

Released 2021-08-18

  • Return entry averages for the past 1, 3, 12 months from the entry count methods. (#249)

  • Use an index for get_entry_counts(feed=...) calls. Makes the /feeds?counts=yes page load 2-4x faster. (#251)

  • Add UpdateResult updated_feed, error, and not_modified convenience properties. (#204)

  • In the web application, show the feed entry count averages as a bar sparkline. (#249)

  • Make the minimum SQLite version and required SQLite compile options reader._storage module globals, for easier monkeypatching. (#163)

    This is allows supplying a user-defined json_array_length function on platforms where SQLite doesn’t come with the JSON1 extension (e.g. on Windows with stock Python earlier than 3.9; details).

    Note these globals are private, and thus not covered by the backwards compatibility policy.

Version 2.0

Released 2021-07-17


This release contains backwards incompatible changes.

  • Remove old database migrations.

    If you are upgrading from reader 1.15 or newer, no action is required.


    If you are upgrading to reader 2.0 from a version older than 1.15, you must open your database with reader 1.15 or newer once, to run the removed migrations:

    pip install 'reader>=1.15,<2' && \
    python - db.sqlite << EOF
    import sys
    from reader import make_reader
  • Remove code that issued deprecation warnings in versions 1.* (#183):

    • Reader.remove_feed()

    • Reader.mark_as_read()

    • Reader.mark_as_unread()

    • Reader.mark_as_important()

    • Reader.mark_as_unimportant()

    • Reader.iter_feed_metadata()

    • the get_feed_metadata(feed, key, default=no value, /) form of Reader.get_feed_metadata()

    • Reader.set_feed_metadata()

    • Reader.delete_feed_metadata()

    • the new_only parameter of update_feeds() and update_feeds_iter()

    • EntryError.url

    • UpdatedFeed.updated

  • The datetime attributes of Feed and Entry objects are now timezone-aware, with the timezone set to utc. Previously, they were naive datetimes representing UTC times. (#233)

  • The parameters of update_feeds() and update_feeds_iter() are now keyword-only. (#183)

  • The feed_root argument of make_reader() now defaults to None (don’t open local feeds) instead of '' (full filesystem access).

  • make_reader() may now raise any ReaderError, not just StorageError.

  • Entry.updated may now be None; use updated_not_none for the pre-2.0 behavior.

Version 1.20

Released 2021-07-12

Version 1.19

Released 2021-06-16

  • Drop Python 3.6 support. (#237)

  • Support PyPy 3.7. (#234)

  • Skip enclosures with no href/url; previously, they would result in a parse error. (#240)

  • Stop using Travis CI (only use GitHub Actions). (#199)

  • Add the new argument to update_feeds() and update_feeds_iter(); new_only is deprecated and will be removed in 2.0. (#217)

  • Rename UpdatedFeed.updated to modified; for backwards compatibility, the old attribute will be available as a property until version 2.0, when it will be removed.. (#241)


    The signature of UpdatedFeed changed from UpdatedFeed(url, new, updated) to UpdatedFeed(url, new, modified).

    This is a minor compatibility break, but only affects third-party code that instantiates UpdatedFeed directly with updated as a keyword argument.

Version 1.18

Released 2021-06-03

  • Rename Reader feed metadata methods:

    • iter_feed_metadata() to get_feed_metadata()

    • get_feed_metadata() to get_feed_metadata_item()

    • set_feed_metadata() to set_feed_metadata_item()

    • delete_feed_metadata() to delete_feed_metadata_item()

    For backwards compatibility, the old method signatures will continue to work until version 2.0, when they will be removed. (#183)


    The get_feed_metadata(feed, key[, default]) -> value form is backwards-compatible only when the arguments are positional.

    This is a minor compatibility break; the following work in 1.17, but do not in 1.18:

    # raises TypeError
    reader.get_feed_metadata(feed, key, default=None)
    # returns `(key, value), ...` instead of `value`
    reader.get_feed_metadata(feed, key=key)

    The pre-1.18 get_feed_metadata() (1.18 get_feed_metadata_item()) is intended to have positional-only arguments, but this cannot be expressed easily until Python 3.8.

  • Rename MetadataNotFoundError to FeedMetadataNotFoundError. MetadataNotFoundError remains available, and is a superclass of FeedMetadataNotFoundError for backwards compatibility. (#228)


    The signatures of the following exceptions changed:


    Takes a new required key argument, instead of no required arguments.


    Takes only one required argument, key; the url argument has been removed.

    Use FeedMetadataNotFoundError instead.

    This is a minor compatibility break, but only affects third-party code that instantiates these exceptions directly.

  • Rename EntryError.url to feed_url; for backwards compatibility, the old attribute will be available as a property until version 2.0, when it will be removed. (#183).


    The signature of EntryError (and its subclasses) changed from EntryError(url, id) to EntryError(feed_url, id).

    This is a minor compatibility break, but only affects third-party code that instantiates these exceptions directly with url as a keyword argument.

  • Rename remove_feed() to delete_feed(). For backwards compatibility, the old method will continue to work until version 2.0, when it will be removed. (#183)

  • Rename Reader mark_as_... methods:

    For backwards compatibility, the old methods will continue to work until version 2.0, when they will be removed. (#183)

  • Fix feeds with no title sometimes missing from the get_feeds() results when there are more than 256 feeds (Storage.chunk_size). (#203)

  • When serving the web application with python -m reader serve, don’t set the Referer header for cross-origin requests. (#209)

Version 1.17

Released 2021-05-06

  • Reserve tags and metadata keys starting with .reader. and .plugin. for reader- and plugin-specific uses. See the Reserved names user guide section for details. (#186)

  • Ignore updated when updating feeds; only update the feed if other feed data changed or if any entries were added/updated. (#231)

    Prevents spurious updates for feeds whose updated changes excessively (either because the entries’ content changes excessively, or because an RSS feed does not have a dc:date element, and feedparser falls back to lastBuildDate for updated).

  • The regex_mark_as_read experimental plugin is now built-in. To use it with the CLI / web application, use the plugin name instead of the entry point (reader.mark_as_read).

    The config metadata key and format changed; the config will be migrated automatically on the next feed update, during reader version 1.17 only. If you used regex_mark_as_read and are upgrading to a version >1.17, install 1.17 (pip install reader==1.17) and run a full feed update (python -m reader update) before installing the newer version.

  • The enclosure-tags, preview-feed-list, and sqlite-releases unstable extras are not available anymore. Use the unstable-plugins extra to install dependencies of the unstable plugins instead.

  • In the web application, allow updating a feed manually. (#195)

Version 1.16

Released 2021-03-29

  • Allow make_reader() to load plugins through the plugins argument. (#229)

    Enable the ua_fallback plugin by default.

    make_reader() may now raise InvalidPluginError (a ValueError subclass, which it already raises implicitly) for invalid plugin names.

  • The enclosure_dedupe, feed_entry_dedupe, and ua_fallback plugins are now built-in. (#229)

    To use them with the CLI / web application, use the plugin name instead of the entry point:

    reader._plugins.enclosure_dedupe:enclosure_dedupe   -> reader.enclosure_dedupe
    reader._plugins.feed_entry_dedupe:feed_entry_dedupe -> reader.entry_dedupe
    reader._plugins.ua_fallback:init                    -> reader.ua_fallback
  • Remove the plugins extra; plugin loading machinery does not have additional dependencies anymore.

  • Mention in the User guide that all reader functions/methods can raise ValueError or TypeError if passed invalid arguments. There is no behavior change, this is just documenting existing, previously undocumented behavior.

Version 1.15

Released 2021-03-21

  • Update entries whenever their content changes, regardless of their updated date. (#179)

    Limit content-only updates (not due to an updated change) to 24 consecutive updates, to prevent spurious updates for entries whose content changes excessively (for example, because it includes the current time). (#225)

    Previously, entries would be updated only if the entry updated was newer than the stored one.

  • Fix bug causing entries that don’t have updated set in the feed to not be updated if the feed is marked as stale. Feed staleness is an internal feature used during storage migrations; this bug could only manifest when migrating from 0.22 to 1.x. (found during #179)

  • Minor web application improvements.

  • Minor CLI improvements.

Version 1.14

Released 2021-02-22

  • Add the update_feeds_iter() method, which yields the update status of each feed as it gets updated. (#204)

  • Change the return type of update_feed() from None to Optional[UpdatedFeed]. (#204)

  • Add the session_timeout argument to make_reader() to set a timeout for retrieving HTTP(S) feeds. The default (connect timeout, read timeout) is (3.05, 60) seconds; the previous behavior was to never time out.

  • Use PRAGMA user_version instead of a version table. (#210)

  • Use PRAGMA application_id to identify reader databases; the id is 0x66656564read in ASCII / UTF-8. (#211)

  • Change the reader update command to show a progress bar and update summary (with colors), instead of plain log output. (#204)

  • Fix broken Mypy config following 0.800 release. (#213)

Version 1.13

Released 2021-01-29

  • JSON Feed support. (#206)

  • Split feed retrieval from parsing; should make it easier to add new/custom parsers. (#206)

  • Prevent any logging output from the reader logger by default. (#207)

  • In the preview_feed_list plugin, add <link rel=alternative ...> tags as a feed detection heuristic.

  • In the preview_feed_list plugin, add <a> tags as a fallback feed detection heuristic.

  • In the web application, fix bug causing the entries page to crash when counts are enabled.

Version 1.12

Released 2020-12-13

  • Add the limit and starting_after arguments to get_feeds(), get_entries(), and search_entries(), allowing them to be used in a paginated fashion. (#196)

  • Add the object_id property that allows getting the unique identifier of a data object in a uniform way. (#196)

  • In the web application, add links to toggle feed/entry counts. (#185)

Version 1.11

Released 2020-11-28

  • Allow disabling feed updates for specific feeds. (#187)

  • Add methods to get aggregated feed and entry counts. (#185)

  • In the web application: allow disabling feed updates for a feed; allow filtering feeds by whether they have updates enabled; do not show feed update errors for feeds that have updates disabled. (#187)

  • In the web application, show feed and entry counts when ?counts=yes is used. (#185)

  • In the web application, use YAML instead of JSON for the tags and metadata fields.

Version 1.10

Released 2020-11-20

  • Use indexes for get_entries() (recent order); should make calls 10-30% faster. (#134)

  • Allow sorting search_entries() results randomly. Allow sorting search results randomly in the web application. (#200)

  • Reraise unexpected errors caused by parser bugs instead of replacing them with an AssertionError.

  • Add the sqlite_releases custom parser plugin.

  • Refactor the HTTP feed sub-parser to allow reuse by custom parsers.

  • Add a user guide, and improve other parts of the documentation. (#194)

Version 1.9

Released 2020-10-28

  • Support Python 3.9. (#199)

  • Support Windows (requires Python >= 3.9). (#163)

  • Use GitHub Actions to do macOS and Windows CI builds. (#199)

  • Rename the cloudflare_ua_fix plugin to ua_fallback. Retry any feed that gets a 403, not just those served by Cloudflare. (#181)

  • Fix type annotation to avoid mypy 0.790 errors. (#198)

Version 1.8

Released 2020-10-02

  • Drop feedparser 5.x support (deprecated in 1.7); use feedparser 6.x instead. (#190)

  • Make the string representation of ReaderError and its subclasses more consistent; add error messages and improve the existing ones. (#173)

  • Add method change_feed_url() to change the URL of a feed. (#149)

  • Allow changing the URL of a feed in the web application. (#149)

  • Add more tag navigation links to the web application. (#184)

  • In the feed_entry_dedupe plugin, copy the important flag from the old entry to the new one. (#140)

Version 1.7

Released 2020-09-19

  • Add new methods to support feed tags: add_feed_tag(), remove_feed_tag(), and get_feed_tags(). Allow filtering feeds and entries by their feed tags. (#184)

  • Add the broken argument to get_feeds(), which allows getting only feeds that failed / did not fail during the last update. (#189)

  • feedparser 5.x support is deprecated in favor of feedparser 6.x. Using feedparser 5.x will raise a deprecation warning in version 1.7, and support will be removed the following version. (#190)

  • Tag-related web application features: show tags in the feed list; allow adding/removing tags; allow filtering feeds and entries by their feed tag; add a page that lists all tags. (#184)

  • In the web application, allow showing only feeds that failed / did not fail. (#189)

  • In the preview_feed_list plugin, add <meta> tags as a feed detection heuristic.

  • Add a few property-based tests. (#188)

Version 1.6

Released 2020-09-04

  • Add the feed_root argument to make_reader(), which allows limiting local feed parsing to a specific directory or disabling it altogether. Using it is recommended, since by default reader will access any local feed path (in 2.0, local file parsing will be disabled by default). (#155)

  • Support loading CLI and web application settings from a configuration file. (#177)

  • Fail fast for feeds that return HTTP 4xx or 5xx status codes, instead of (likely) failing later with an ambiguous XML parsing error. The cause of the raised ParseError is now an instance of requests.HTTPError. (#182)

  • Add cloudflare_ua_fix plugin (work around Cloudflare sometimes blocking requests). (#181)

  • feedparser 6.0 (beta) compatibility fixes.

  • Internal parser API changes to support alternative parsers, pre-request hooks, and making arbitrary HTTP requests using the same logic Reader uses. (#155)

  • In the /preview page and the preview_feed_list plugin, use the same plugins the main Reader does. (enabled by #155)

Version 1.5

Released 2020-07-30

  • Use rowid when deleting from the search index, instead of the entry id. Previously, each update_search() call would result in a full scan, even if there was nothing to update/delete. This should reduce the amount of reads significantly (deleting 4 entries from a database with 10k entries resulted in an 1000x decrease in bytes read). (#178)

  • Require at least SQLite 3.18 (released 2017-03-30) for the current update_search() implementation; all other reader features continue to work with SQLite >= 3.15. (#178)

  • Run PRAGMA optimize on close(). This should increase the performance of all methods. As an example, in #178 it was found that update_search() resulted in a full scan of the entries table, even if there was nothing to update; this change should prevent this from happening. (#143)


    PRAGMA optimize is a no-op in SQLite versions earlier than 3.18. In order to avoid the case described above, you should run ANALYZE regularly (e.g. every few days).

Version 1.4

Released 2020-07-13

  • Work to reduce the likelihood of “database is locked” errors during updates (#175):

    • Prepare entries to be added to the search index (update_search()) outside transactions.

    • Fix bug causing duplicate rows in the search index when an entry changes while updating the search index.

    • Update the search index only when the indexed values change (details below).

    • Use SQLite WAL (details below).

  • Update the search index only when the indexed values change. Previously, any change on a feed would result in all its entries being re-indexed, even if the feed title or the entry content didn’t change. This should reduce the update_search() run time significantly.

  • Use SQLite’s write-ahead logging to increase concurrency. At the moment there is no way to disable WAL. This change may be reverted in the future. (#169)

  • Require at least click 7.0 for the cli extra.

  • Do not fail for feeds with incorrectly-declared media types, if feedparser can parse the feed; this is similar to the current behavior for incorrectly-declared encodings. (#171)

  • Raise ParseError during update for feeds feedparser can’t detect the type of, instead of silently returning an empty feed. (#171)

  • Add sort argument to search_entries(). Allow sorting search results by recency in addition to relevance (the default). (#176)

  • In the web application, display a nice error message for invalid search queries instead of returning an HTTP 500 Internal Server Error.

  • Other minor web application improvements.

  • Minor CLI logging improvements.

Version 1.3

Released 2020-06-23

  • If a feed failed to update, provide details about the error in Feed.last_exception. (#68)

  • Show details about feed update errors in the web application. (#68)

  • Expose the added and last_updated Feed attributes.

  • Expose the last_updated Entry attribute.

  • Raise ParseError / log during update if an entry has no id, instead of unconditionally raising AttributeError. (#170)

  • Fall back to <link> as entry id if an entry in an RSS feed has no <guid>; previously, feeds like this would fail on update. (#170)

  • Minor web application improvements (show feed added/updated date).

  • In the web application, handle previewing an invalid feed nicely instead of returning an HTTP 500 Internal Server Error. (#172)

  • Internal API changes to support multiple storage implementations in the future. (#168)

Version 1.2

Released 2020-05-18

  • Minor web application improvements.

  • Remove unneeded additional query in methods that use pagination (for n = len(result) / page size, always do n queries instead n+1). get_entries() and search_entries() are now 33–7% and 46–36% faster, respectively, for results of size 32–256. (#166)

  • All queries are now chunked/paginated to avoid locking the SQLite storage for too long, decreasing the chance of concurrent queries timing out; the problem was most visible during update_search(). This should cap memory usage for methods returning an iterable that were not paginated before; previously the whole result set would be read before returning it. (#167)

Version 1.1

Released 2020-05-08

  • Add sort argument to get_entries(). Allow sorting entries randomly in addition to the default most-recent-first order. (#105)

  • Allow changing the entry sort order in the web application. (#105)

  • Use a query builder instead of appending strings manually for the more complicated queries in search and storage. (#123)

  • Make searching entries faster by filtering them before searching; e.g. if 1/5 of the entries are read, searching only read entries is now ~5x faster. (enabled by #123)

Version 1.0.1

Released 2020-04-30

  • Fix bug introduced in 0.20 causing update_feeds() to silently stop updating the remaining feeds after a feed failed. (#164)

Version 1.0

Released 2020-04-28

  • Make all private submodules explicitly private. (#156)


    All direct imports from reader continue to work.

    • The reader.core.* modules moved to reader.* (most of them prefixed by _).

    • The web application WSGI entry point moved from to reader._app.wsgi:app.

    • The entry points for plugins that ship with reader moved from reader.plugins.* to reader._plugins.*.

  • Require at least beautifulsoup4 4.5 for the search extra (before, the version was unspecified). (#161)

  • Rename the web application dependencies extra from web-app to app.

  • Fix relative link resolution and content sanitization; sgmllib3k is now a required dependency for this reason. (#125, #157)

Version 0.22

Released 2020-04-14

  • Add the Entry.feed_url attribute. (#159)

  • Rename the EntrySearchResult feed attribute to feed_url. Using feed will raise a deprecation warning in version 0.22, and will be removed in the following version. (#159)

  • Use executemany() instead of execute() in the SQLite storage. Makes updating feeds (excluding network calls) 5-10% faster. (#144)

  • In the web app, redirect to the feed’s page after adding a feed. (#119)

  • In the web app, show highlighted search result snippets. (#122)

Version 0.21

Released 2020-04-04

  • Minor consistency improvements to the web app search button. (#122)

  • Add support for web application plugins. (#80)

  • The enclosure tag proxy is now a plugin, and is disabled by default. See its documentation for details. (#52)

  • In the web app, the “add feed” button shows a preview before adding the feed. (#145)

  • In the web app, if the feed to be previewed is not actually a feed, show a list of feeds linked from that URL. This is a plugin, and is disabled by default. (#150)

  • reader now uses a User-Agent header like python-reader/0.21 when retrieving feeds instead of the default requests one. (#154)

Version 0.20

Released 2020-03-31

  • Fix bug in enable_search() that caused it to fail if search was already enabled and the reader had any entries.

  • Add an entry argument to get_entries(), for symmetry with search_entries().

  • Add a feed argument to get_feeds().

  • Add a key argument to get_feed_metadata().

  • Require at least requests 2.18 (before, the version was unspecified).

  • Allow updating feeds concurrently; add a workers argument to update_feeds(). (#152)

Version 0.19

Released 2020-03-25

  • Support PyPy 3.6.

  • Allow searching for entries. (#122)

  • Stricter type checking for the core modules.

  • Various changes to the storage internal API.

Version 0.18

Released 2020-01-26

  • Support Python 3.8.

  • Increase the get_entries() recent threshold from 3 to 7 days. (#141)

  • Enforce type checking for the core modules. (#132)

  • Use dataclasses for the data objects instead of attrs. (#137)

Version 0.17

Released 2019-10-12

  • Remove the which argument of get_entries(). (#136)

  • Reader objects should now be created using make_reader(). Instantiating Reader directly will raise a deprecation warning.

  • The resources associated with a reader can now be released explicitly by calling its close() method. (#139)

  • Make the database schema more strict regarding nulls. (#138)

  • Tests are now run in a random order. (#142)

Version 0.16

Released 2019-09-02

Version 0.15

Released 2019-08-24

  • Improve entry page rendering for text/plain content. (#117)

  • Improve entry page rendering for images and code blocks. (#126)

  • Show enclosures on the entry page. (#128)

  • Show the entry author. (#129)

  • Fix bug causing the enclosure tag proxy to use too much memory. (#133)

  • Start using mypy on the core modules. (#132)

Version 0.14

Released 2019-08-12

  • Drop Python 3.5 support. (#124)

  • Improve entry ordering implementation. (#110)

Version 0.13

Released 2019-07-12

Version 0.12

Released 2019-06-22

  • Fix flashed messages never disappearing. (#81)

  • Minor metadata page UI improvements.

  • Allow limiting the number of entries on the entries page via the limit URL parameter.

  • Add link to the feed on the entries and feeds pages. (#118)

  • Use Black and pre-commit to enforce style.

Version 0.11

Released 2019-05-26

  • Support storing per-feed metadata. (#114)

  • Add feed metadata page to the web app. (#114)

  • The regex_mark_as_read plugin is now configurable via feed metadata; drop support for the READER_PLUGIN_REGEX_MARK_AS_READ_CONFIG file. (#114)

Version 0.10

Released 2019-05-18

  • Unify plugin loading and error handling code. (#112)

  • Minor improvements to CLI error reporting.

Version 0.9

Released 2019-05-12

  • Improve the get_entries() sorting algorithm. Fixes a bug introduced by #106 (entries of new feeds would always show up at the top). (#113)

Version 0.8

Released 2019-04-21

  • Make the internal APIs use explicit types instead of tuples. (#111)

  • Finish updater internal API. (#107)

  • Automate part of the release process (scripts/

Version 0.7

Released 2019-04-14

  • Increase timeout of the button actions from 2 to 10 seconds.

  • get_entries() now sorts entries by the import date first, and then by published/updated. (#106)

  • Add enclosure_dedupe plugin (deduplicate enclosures of an entry). (#78)

  • The serve command now supports loading plugins. (#78)

  • now supports loading plugins. (#78)

Version 0.6

Released 2019-04-13

  • Minor web application style changes to make the layout more condensed.

  • Factor out update logic into a separate interface. (#107)

  • Fix update failing if the feed does not have a content type header. (#108)

Version 0.5

Released 2019-02-09

  • Make updating new feeds up to 2 orders of magnitude faster; fixes a problem introduced by #94. (#104)

  • Move the core modules to a separate subpackage and enforce test coverage (make coverage now fails if the coverage for core modules is less than 100%). (#101)

  • Support Python 3.8 development branch.

  • Add dev and docs extras (to install development requirements).

  • Build HTML documentation when running tox.

  • Add test-all and docs make targets (to run tox / build HTML docs).

Version 0.4

Released 2019-01-02

Version 0.3

Released on 2018-12-22

  • get_entries() now prefers sorting by published (if present) to sorting by updated. (#97)

  • Add regex_mark_as_read plugin (mark new entries as read based on a regex). (#79)

  • Add feed_entry_dedupe plugin (deduplicate new entries for a feed). (#79)

  • Plugin loading machinery dependencies are now installed via the plugins extra.

  • Add a plugins section to the documentation.

Version 0.2

Released on 2018-11-25

  • Factor out storage-related functionality into a separate interface. (#94)

  • Fix update --new-only updating the same feed repeatedly on databases that predate --new-only. (#95)

  • Add web application screenshots to the documentation.

Version 0.1.1

Released on 2018-10-21

  • Fix broken reader serve command (broken in 0.1).

  • Raise StorageError for unsupported SQLite configurations at Reader instantiation instead of failing at run-time with a generic StorageError("sqlite3 error"). (#92)

  • Fix wrong submit button being used when pressing enter in non-button fields. (#69)

  • Raise StorageError for failed migrations instead of an undocumented exception. (#92)

  • Use requests-mock in parser tests instead of a web server (test suite run time down by ~35%). (#90)

Version 0.1

Released on 2018-09-15

  • Initial release; public API stable.

  • Support broken Tumblr feeds via the the tumblr_gdpr plugin. (#67)