Skip to content

Commit

Permalink
Support entry and global tags (untested).
Browse files Browse the repository at this point in the history
For #272.
  • Loading branch information
lemon24 committed Feb 26, 2022
1 parent 94cb0af commit 43c97ac
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 65 deletions.
39 changes: 25 additions & 14 deletions src/reader/_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .exceptions import ReaderError
from .exceptions import StorageError
from .exceptions import TagNotFoundError
from .types import AnyResourceId
from .types import Content
from .types import Enclosure
from .types import Entry
Expand All @@ -57,6 +58,7 @@
from .types import JSONType
from .types import MISSING
from .types import MissingType
from .types import ResourceId

APPLICATION_ID = int(''.join(f'{ord(c):x}' for c in 'read'), 16)

Expand Down Expand Up @@ -1125,7 +1127,7 @@ def get_entry_counts(

def get_tags(
self,
object_id: Tuple[Optional[str], ...],
object_id: AnyResourceId,
key: Optional[str] = None,
) -> Iterable[Tuple[str, JSONType]]:
yield from join_paginated_iter(
Expand All @@ -1136,23 +1138,32 @@ def get_tags(
@wrap_exceptions_iter(StorageError)
def get_tags_page(
self,
object_id: Tuple[Optional[str], ...],
object_id: AnyResourceId,
key: Optional[str] = None,
chunk_size: Optional[int] = None,
last: Optional[_T] = None,
) -> Iterable[Tuple[Tuple[str, JSONType], Optional[_T]]]:
info = SCHEMA_INFO[len(object_id)]

query = Query().SELECT("key").FROM(f"{info.table_prefix}tags")
query = Query().SELECT("key")
context: Dict[str, Any] = dict()

if not any(p is None for p in object_id):
query.SELECT("value")
for column in info.id_columns:
query.WHERE(f"{column} = :{column}")
context.update(zip(info.id_columns, object_id))
if object_id is not None:
info = SCHEMA_INFO[len(object_id)]
query.FROM(f"{info.table_prefix}tags")

if not any(p is None for p in object_id):
query.SELECT("value")
for column in info.id_columns:
query.WHERE(f"{column} = :{column}")
context.update(zip(info.id_columns, object_id))
else:
query.SELECT_DISTINCT("'null'")

else:
union = '\nUNION\n'.join(
f"SELECT key, value FROM {i.table_prefix}tags"
for i in SCHEMA_INFO.values()
)
query.WITH(('tags', union)).FROM('tags')
query.SELECT_DISTINCT("'null'")

if key is not None:
Expand All @@ -1170,19 +1181,19 @@ def row_factory(t: Tuple[Any, ...]) -> Tuple[str, JSONType]:
)

@overload
def set_tag(self, object_id: Tuple[str, ...], key: str) -> None: # pragma: no cover
def set_tag(self, object_id: ResourceId, key: str) -> None: # pragma: no cover
...

@overload
def set_tag(
self, object_id: Tuple[str, ...], key: str, value: JSONType
self, object_id: ResourceId, key: str, value: JSONType
) -> None: # pragma: no cover
...

@wrap_exceptions(StorageError)
def set_tag(
self,
object_id: Tuple[str, ...],
object_id: ResourceId,
key: str,
value: Union[MissingType, JSONType] = MISSING,
) -> None:
Expand Down Expand Up @@ -1227,7 +1238,7 @@ def set_tag(
raise info.not_found_exc(*object_id) from None

@wrap_exceptions(StorageError)
def delete_tag(self, object_id: Tuple[str, ...], key: str) -> None:
def delete_tag(self, object_id: ResourceId, key: str) -> None:
info = SCHEMA_INFO[len(object_id)]

columns = info.id_columns + ('key',)
Expand Down
72 changes: 44 additions & 28 deletions src/reader/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
from .plugins import DEFAULT_PLUGINS
from .types import _entry_argument
from .types import _feed_argument
from .types import _resource_argument
from .types import AnyResourceId
from .types import AnyResourceInput
from .types import Entry
from .types import EntryCounts
from .types import EntryInput
Expand All @@ -68,6 +71,7 @@
from .types import JSONType
from .types import MISSING
from .types import MissingType
from .types import ResourceInput
from .types import SearchSortOrder
from .types import TagFilterInput
from .types import UpdatedFeed
Expand Down Expand Up @@ -1976,11 +1980,13 @@ def get_feed_tags(
.. versionadded:: 1.7
"""
return (k for k, _ in self.get_tags(feed))
return self.get_tag_keys(feed)

# FIXME: no wildcards allowed in get_tags, update docstring/changelog

def get_tags(
self,
resource: Union[FeedInput, None, Tuple[None]],
resource: ResourceInput,
*,
key: Optional[str] = None,
) -> Iterable[Tuple[str, JSONType]]:
Expand All @@ -2003,19 +2009,12 @@ def get_tags(
.. versionadded:: 2.8
"""
if resource is None:
feed_url = None
elif isinstance(resource, tuple):
if resource != (None,):
raise ValueError(f"invalid resource: {resource!r}")
feed_url = None
else:
feed_url = _feed_argument(resource)
return self._storage.get_tags((feed_url,), key)
resource_id = _resource_argument(resource)
return self._storage.get_tags(resource_id, key)

def get_tag_keys(
self,
resource: Union[FeedInput, None, Tuple[None]] = None,
resource: AnyResourceInput = None,
) -> Iterable[str]: # pragma: no cover
"""Get the keys of all or some resource tags.
Expand All @@ -2038,20 +2037,34 @@ def get_tag_keys(
"""
# FIXME: cover
# TODO: (later) efficient implementation
return (k for k, _ in self.get_tags(resource))
resource_id: AnyResourceId
if resource is None:
resource_id = None
elif resource == (None,):
resource_id = (None,)
elif resource == (None, None):
resource_id = (None, None)
else:
resource_id = _resource_argument(resource) # type: ignore[arg-type]
return (k for k, _ in self._storage.get_tags(resource_id))

@overload
def get_tag(self, resource: FeedInput, key: str) -> JSONType: # pragma: no cover
def get_tag(
self, resource: ResourceInput, key: str
) -> JSONType: # pragma: no cover
...

@overload
def get_tag(
self, resource: FeedInput, key: str, default: _T
self, resource: ResourceInput, key: str, default: _T
) -> Union[JSONType, _T]: # pragma: no cover
...

def get_tag(
self, resource: FeedInput, key: str, default: Union[MissingType, _T] = MISSING
self,
resource: ResourceInput,
key: str,
default: Union[MissingType, _T] = MISSING,
) -> Union[JSONType, _T]:
"""Get the value of this resource tag.
Expand All @@ -2074,25 +2087,27 @@ def get_tag(
.. versionadded:: 2.8
"""
resource_id = _resource_argument(resource)
object_id: Any = resource_id if len(resource_id) != 1 else resource_id[0] # type: ignore
return zero_or_one(
(v for _, v in self.get_tags(resource, key=key)),
lambda: TagNotFoundError(key, _feed_argument(resource)),
(v for _, v in self._storage.get_tags(resource_id, key)),
lambda: TagNotFoundError(key, object_id),
default,
)

@overload
def set_tag(self, resource: FeedInput, key: str) -> None: # pragma: no cover
def set_tag(self, resource: ResourceInput, key: str) -> None: # pragma: no cover
...

@overload
def set_tag(
self, resource: FeedInput, key: str, value: JSONType
self, resource: ResourceInput, key: str, value: JSONType
) -> None: # pragma: no cover
...

def set_tag(
self,
resource: FeedInput,
resource: ResourceInput,
key: str,
value: Union[JSONType, MissingType] = MISSING,
) -> None:
Expand All @@ -2114,14 +2129,14 @@ def set_tag(
.. versionadded:: 2.8
"""
feed_url = _feed_argument(resource)
resource_id = _resource_argument(resource)
if not isinstance(value, MissingType):
self._storage.set_tag((feed_url,), key, value)
self._storage.set_tag(resource_id, key, value)
else:
self._storage.set_tag((feed_url,), key)
self._storage.set_tag(resource_id, key)

def delete_tag(
self, resource: FeedInput, key: str, missing_ok: bool = False
self, resource: ResourceInput, key: str, missing_ok: bool = False
) -> None:
"""Delete this resource tag.
Expand All @@ -2139,12 +2154,13 @@ def delete_tag(
.. versionadded:: 2.8
"""
feed_url = _feed_argument(resource)
resource_id = _resource_argument(resource)
object_id: Any = resource_id if len(resource_id) != 1 else resource_id[0] # type: ignore
try:
self._storage.delete_tag((feed_url,), key)
self._storage.delete_tag(resource_id, key)
except TagNotFoundError as e:
if not missing_ok:
e.object_id = feed_url
e.object_id = object_id
raise

def make_reader_reserved_name(self, key: str) -> str:
Expand Down
5 changes: 4 additions & 1 deletion src/reader/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ class TagError(ReaderError):
"""

def __init__(
self, key: str, object_id: Union[str, Tuple[str, str]], message: str = ''
self,
key: str,
object_id: Union[Tuple[()], str, Tuple[str, str]],
message: str = '',
) -> None:
super().__init__(message)

Expand Down
58 changes: 51 additions & 7 deletions src/reader/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Tuple
from typing import Type
Expand Down Expand Up @@ -639,28 +640,71 @@ def feed_url(self) -> str: # pragma: no cover
...


# https://github.com/lemon24/reader/issues/266#issuecomment-1013739526
GlobalInput = Tuple[()]
FeedInput = Union[str, FeedLike]
EntryInput = Union[Tuple[str, str], EntryLike]
ResourceInput = Union[GlobalInput, FeedInput, EntryInput]
AnyResourceInput = Union[ResourceInput, None, Tuple[None], Tuple[None, None]]
ResourceId = Union[Tuple[()], Tuple[str], Tuple[str, str]]
AnyResourceId = Union[ResourceId, None, Tuple[None], Tuple[None, None]]


def _feed_argument(feed: FeedInput) -> str:
if isinstance(feed, FeedLike):
return feed.url
if isinstance(feed, str):
return feed
rv = feed.url
elif isinstance(feed, tuple) and len(feed) == 1:
rv = feed[0]
else:
rv = feed
if isinstance(rv, str):
return rv
raise ValueError(f'invalid feed argument: {feed!r}')


def _entry_argument(entry: EntryInput) -> Tuple[str, str]:
if isinstance(entry, EntryLike):
return _feed_argument(entry.feed_url), entry.id
if isinstance(entry, tuple) and len(entry) == 2:
feed_url, entry_id = entry
rv = _feed_argument(entry.feed_url), entry.id
elif isinstance(entry, tuple) and len(entry) == 2:
rv = entry
else:
rv = None
if rv:
feed_url, entry_id = rv
if isinstance(feed_url, str) and isinstance(entry_id, str):
return entry
return rv
raise ValueError(f'invalid entry argument: {entry!r}')


@overload
def _resource_argument(resource: GlobalInput) -> Tuple[()]:
... # pragma: no cover


@overload
def _resource_argument(resource: FeedInput) -> Tuple[str]:
... # pragma: no cover


@overload
def _resource_argument(resource: EntryInput) -> Tuple[str, str]:
... # pragma: no cover


def _resource_argument(resource: ResourceInput) -> ResourceId:
if isinstance(resource, tuple) and len(resource) == 0:
return resource
try:
return (_feed_argument(resource),) # type: ignore[arg-type]
except ValueError:
pass
try:
return _entry_argument(resource) # type: ignore[arg-type]
except ValueError:
pass
raise ValueError(f"invalid resource argument: {resource!r}")


# str explicitly excluded, to allow for a string-based query language;
# https://github.com/lemon24/reader/issues/184#issuecomment-689587006
TagFilterInput = Union[
Expand Down
7 changes: 3 additions & 4 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2224,10 +2224,9 @@ def test_tags_as_tags(reader, chunk_size):
assert list(reader.get_tag_keys('two')) == []
assert list(reader.get_tag_keys()) == ['tag-1', 'tag-common']

with pytest.raises(ValueError):
list(reader.get_tag_keys(()))
with pytest.raises(ValueError):
list(reader.get_tag_keys(('a', 'b')))
# TODO: test wildcards
assert list(reader.get_tag_keys(())) == []
assert list(reader.get_tag_keys(('a', 'b'))) == []


def test_set_arg_noop(reader):
Expand Down
Loading

0 comments on commit 43c97ac

Please sign in to comment.