Skip to content

Commit

Permalink
Add support for specifying the Palace api-version in mimetype header …
Browse files Browse the repository at this point in the history
…when retrieving opds1 and opds2 client feeds.
  • Loading branch information
dbernstein committed Oct 25, 2024
1 parent 2e11018 commit 55646cc
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 17 deletions.
10 changes: 7 additions & 3 deletions src/palace/manager/feed/opds.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from palace.manager.core.exceptions import BasePalaceException
from palace.manager.feed.base import FeedInterface
from palace.manager.feed.serializer.base import SerializerInterface
from palace.manager.feed.serializer.opds import OPDS1Serializer
from palace.manager.feed.serializer.opds import (
OPDS1Version1Serializer,
OPDS1Version2Serializer,
)
from palace.manager.feed.serializer.opds2 import OPDS2Serializer
from palace.manager.feed.types import FeedData, WorkEntry
from palace.manager.sqlalchemy.model.lane import FeaturedFacets
Expand All @@ -21,7 +24,8 @@ def get_serializer(
) -> SerializerInterface[Any]:
# Ordering matters for poor matches (eg. */*), so we will keep OPDS1 first
serializers: dict[str, type[SerializerInterface[Any]]] = {
"application/atom+xml": OPDS1Serializer,
"application/atom+xml": OPDS1Version1Serializer,
"application/atom+xml; api-version: 2": OPDS1Version2Serializer,
"application/opds+json": OPDS2Serializer,
}
if mime_types:
Expand All @@ -30,7 +34,7 @@ def get_serializer(
)
return serializers[match]()
# Default
return OPDS1Serializer()
return OPDS1Version1Serializer()


class BaseOPDSFeed(FeedInterface):
Expand Down
10 changes: 9 additions & 1 deletion src/palace/manager/feed/serializer/opds.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def is_sort_link(link: Link) -> bool:
)


class OPDS1Serializer(SerializerInterface[etree._Element], OPDSFeed):
class OPDS1Version1Serializer(SerializerInterface[etree._Element], OPDSFeed):
"""An OPDS 1.2 Atom feed serializer"""

def __init__(self) -> None:
Expand Down Expand Up @@ -415,3 +415,11 @@ def _serialize_sort_link(self, link: Link) -> etree._Element:
if link.get("activeFacet", False):
sort_link.add_attributes(dict(activeSort="true"))
return self._serialize_feed_entry("link", sort_link)


class OPDS1Version2Serializer(OPDS1Version1Serializer):
"""An OPDS 1.2 Atom feed serializer with Palace specific modifications (version 2) to support
new IOS and Android client features."""

def __init__(self) -> None:
pass
40 changes: 35 additions & 5 deletions tests/manager/feed/test_opds_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from flask import Request

from palace.manager.feed.opds import get_serializer
from palace.manager.feed.serializer.opds import OPDS1Serializer
from palace.manager.feed.serializer.opds import (
OPDS1Version1Serializer,
OPDS1Version2Serializer,
)
from palace.manager.feed.serializer.opds2 import OPDS2Serializer


Expand All @@ -28,7 +31,9 @@ def test_get_serializer(self):
Accept="application/atom+xml;profile=opds-catalog;kind=acquisition"
)
)
assert isinstance(get_serializer(request.accept_mimetypes), OPDS1Serializer)
assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version1Serializer
)

# The default q-value should be 1, but opds2 specificity is higher
request = Request.from_values(
Expand All @@ -42,16 +47,41 @@ def test_get_serializer(self):
request = Request.from_values(
headers=dict(Accept="application/opds+json;q=0.9,application/atom+xml")
)
assert isinstance(get_serializer(request.accept_mimetypes), OPDS1Serializer)
assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version1Serializer
)

# Same q-values respect order of definition in the code
request = Request.from_values(
headers=dict(
Accept="application/opds+json;q=0.9,application/atom+xml;q=0.9"
)
)
assert isinstance(get_serializer(request.accept_mimetypes), OPDS1Serializer)
assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version1Serializer
)

# test api-version parameter when specified return the appropriate
# version
request = Request.from_values(
headers=dict(Accept="application/atom+xml; api-version:1")
)

assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version1Serializer
)

# test api-version parameter
request = Request.from_values(
headers=dict(Accept="application/atom+xml; api-version:2")
)

assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version2Serializer
)

# No valid accept mimetype should default to OPDS1.x
request = Request.from_values(headers=dict(Accept="text/html"))
assert isinstance(get_serializer(request.accept_mimetypes), OPDS1Serializer)
assert isinstance(
get_serializer(request.accept_mimetypes), OPDS1Version1Serializer
)
16 changes: 8 additions & 8 deletions tests/manager/feed/test_opds_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytz
from lxml import etree

from palace.manager.feed.serializer.opds import OPDS1Serializer, is_sort_link
from palace.manager.feed.serializer.opds import OPDS1Version1Serializer, is_sort_link
from palace.manager.feed.serializer.opds2 import PALACE_REL_SORT
from palace.manager.feed.types import (
Acquisition,
Expand All @@ -22,7 +22,7 @@ def test__serialize_feed_entry(self):
child = FeedEntryType.create(text="child", attr="chattr", grandchild=grandchild)
parent = FeedEntryType.create(text="parent", attr="pattr", child=child)

serialized = OPDS1Serializer()._serialize_feed_entry("parent", parent)
serialized = OPDS1Version1Serializer()._serialize_feed_entry("parent", parent)

assert serialized.tag == "parent"
assert serialized.text == "parent"
Expand Down Expand Up @@ -50,7 +50,7 @@ def test__serialize_author_tag(self):
lc="lc",
)

element = OPDS1Serializer()._serialize_author_tag("author", author)
element = OPDS1Version1Serializer()._serialize_author_tag("author", author)

assert element.tag == "author"
assert element.get(f"{{{OPDSFeed.OPF_NS}}}role") == author.role
Expand Down Expand Up @@ -102,7 +102,7 @@ def test__serialize_acquistion_link(self):
vendor="vendor", clientToken=FeedEntryType(text="token")
),
)
element = OPDS1Serializer()._serialize_acquistion_link(link)
element = OPDS1Version1Serializer()._serialize_acquistion_link(link)
assert element.tag == "link"
assert dict(element.attrib) == dict(href=link.href)

Expand Down Expand Up @@ -159,7 +159,7 @@ def test_serialize_work_entry(self):
duration=10,
)

element = OPDS1Serializer().serialize_work_entry(data)
element = OPDS1Version1Serializer().serialize_work_entry(data)

assert (
element.get(f"{{{OPDSFeed.SCHEMA_NS}}}additionalType")
Expand Down Expand Up @@ -246,21 +246,21 @@ def test_serialize_work_entry(self):

def test_serialize_work_entry_empty(self):
# A no-data work entry
element = OPDS1Serializer().serialize_work_entry(WorkEntryData())
element = OPDS1Version1Serializer().serialize_work_entry(WorkEntryData())
# This will create an empty <entry> tag
assert element.tag == "entry"
assert list(element) == []

def test_serialize_opds_message(self):
message = OPDSMessage("URN", 200, "Description")
serializer = OPDS1Serializer()
serializer = OPDS1Version1Serializer()
result = serializer.serialize_opds_message(message)
assert serializer.to_string(result) == serializer.to_string(message.tag)

def test_serialize_sort_link(self):
link = Link(href="test", rel="test_rel", title="text1")
link.add_attributes(dict(facetGroup="Sort by", activeFacet="true"))
serializer = OPDS1Serializer()
serializer = OPDS1Version1Serializer()
assert is_sort_link(link)
sort_link = serializer._serialize_sort_link(link)
assert sort_link.attrib["title"] == "text1"
Expand Down

0 comments on commit 55646cc

Please sign in to comment.