224 lines
6.9 KiB
Python
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,
|
|
)
|