feedvault.se/feeds/get_reader.py
2024-05-20 04:34:51 +02:00

224 lines
6.9 KiB
Python

from __future__ import annotations
import logging
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Self
from django.db.models import Q
from reader import ExceptionInfo, FeedExistsError, FeedNotFoundError, Reader, make_reader
from reader._types import (
EntryForUpdate, # noqa: PLC2701
EntryUpdateIntent,
FeedData,
FeedFilter,
FeedForUpdate, # noqa: PLC2701
FeedUpdateIntent,
SearchType, # noqa: PLC2701
StorageType, # noqa: PLC2701
)
from .models import Entry, Feed
if TYPE_CHECKING:
import datetime
from django.db.models.manager import BaseManager
logger = logging.getLogger(__name__)
class EmptySearch(SearchType): ...
class EntriesForUpdateIterator:
def __init__(self, entries: Iterable[tuple[str, str]]) -> None:
self.entries: Iterator[tuple[str, str]] = iter(entries)
def __iter__(self) -> Self:
return self
def __next__(self) -> EntryForUpdate:
try:
feed_url, entry_id = next(self.entries)
except StopIteration:
raise StopIteration from None
print(f"{feed_url=}, {entry_id=}") # noqa: T201
entry_data: dict[str, Any] | None = (
Entry.objects.filter(Q(feed__url=feed_url) & Q(id=entry_id))
.values("updated", "published", "data_hash", "data_hash_changed")
.first()
)
if not entry_data:
return None
return EntryForUpdate(
updated=entry_data.get("updated"),
published=entry_data.get("published"),
hash=entry_data.get("data_hash"),
hash_changed=entry_data.get("data_hash_changed"),
)
class DjangoStorage(StorageType):
# TODO(TheLovinator): Implement all methods from StorageType.
default_search_cls = EmptySearch
def __enter__(self: DjangoStorage) -> None:
"""Called when Reader is used as a context manager."""
# TODO(TheLovinator): Should we check if we have migrations to apply?
def __exit__(self: DjangoStorage, *_: object) -> None:
"""Called when Reader is used as a context manager."""
# TODO(TheLovinator): Should we close the connection?
def close(self: DjangoStorage) -> None:
"""Called by Reader.close()."""
# TODO(TheLovinator): Should we close the connection?
def add_feed(self, url: str, /, added: datetime.datetime) -> None:
"""Called by Reader.add_feed().
Args:
url: The URL of the feed.
added: The time the feed was added.
Raises:
FeedExistsError: Feed already exists. Bases: FeedError
"""
if Feed.objects.filter(url=url).exists():
msg: str = f"Feed already exists: {url}"
raise FeedExistsError(msg)
feed = Feed(url=url, added=added)
feed.save()
def get_feeds_for_update(self, filter: FeedFilter): # noqa: A002
"""Called by update logic.
Args:
filter: The filter to apply.
Returns:
A lazy iterable.
"""
logger.debug(f"{filter=}") # noqa: G004
feeds: BaseManager[Feed] = Feed.objects.all() # TODO(TheLovinator): Don't get all values, use filter.
for feed in feeds:
yield FeedForUpdate(
url=feed.url,
updated=feed.updated,
http_etag=feed.http_etag,
http_last_modified=feed.http_last_modified,
stale=feed.stale,
last_updated=feed.last_updated,
last_exception=bool(feed.last_exception_type_name),
hash=feed.data_hash,
)
def update_feed(self, intent: FeedUpdateIntent, /) -> None:
"""Called by update logic.
Args:
intent: Data to be passed to Storage when updating a feed.
Raises:
FeedNotFoundError
"""
feed: Feed = Feed.objects.get(url=intent.url)
if feed is None:
msg: str = f"Feed not found: {intent.url}"
raise FeedNotFoundError(msg)
feed.last_updated = intent.last_updated
feed.http_etag = intent.http_etag
feed.http_last_modified = intent.http_last_modified
feed_data: FeedData | None = intent.feed
if feed_data is not None:
feed.title = feed_data.title
feed.link = feed_data.link
feed.author = feed_data.author
feed.subtitle = feed_data.subtitle
feed.version = feed_data.version
if intent.last_exception is not None:
last_exception: ExceptionInfo = intent.last_exception
feed.last_exception_type_name = last_exception.type_name
feed.last_exception_value = last_exception.value_str
feed.last_exception_traceback = last_exception.traceback_str
feed.save()
def set_feed_stale(self, url: str, stale: bool, /) -> None: # noqa: FBT001
"""Used by update logic tests.
Args:
url: The URL of the feed.
stale: Whether the next update should update all entries, regardless of their hash or updated.
Raises:
FeedNotFoundError
"""
feed: Feed = Feed.objects.get(url=url)
if feed is None:
msg: str = f"Feed not found: {url}"
raise FeedNotFoundError(msg)
feed.stale = stale
feed.save()
def get_entries_for_update(self, entries: Iterable[tuple[str, str]], /) -> EntriesForUpdateIterator:
for feed_url, entry_id in entries:
logger.debug(f"{feed_url=}, {entry_id=}") # noqa: G004
entries_list = list(entries)
print(f"{entries_list=}") # noqa: T201
return EntriesForUpdateIterator(entries)
def add_or_update_entries(self, intents: Iterable[EntryUpdateIntent], /) -> None:
"""Called by update logic.
Args:
intents: Data to be passed to Storage when updating a feed.
Raises:
FeedNotFoundError
"""
msg = "Not implemented yet."
raise NotImplementedError(msg)
for intent in intents:
feed_id, entry_id = intent.entry.resource_id
logger.debug(f"{feed_id=}, {entry_id=}") # noqa: G004
# TODO(TheLovinator): Implement this method. Use Entry.objects.get_or_create()/Entry.objects.bulk_create()?
# TODO(TheLovinator): Raise FeedNotFoundError if feed does not exist.
def make_search(self) -> SearchType:
"""Called by Reader.make_search().
Returns:
A Search instance.
"""
return EmptySearch()
@lru_cache(maxsize=1)
def get_reader() -> Reader:
"""Create a Reader instance.
reader = get_reader()
reader.add_feed("https://example.com/feed", added=datetime.datetime.now())
reader.update_feeds()
Returns:
A Reader instance.
"""
return make_reader(
"",
_storage=DjangoStorage(),
search_enabled=False,
)