diff --git a/src/webpub_manifest_parser/core/parsers.py b/src/webpub_manifest_parser/core/parsers.py index 172914e..706e7cd 100644 --- a/src/webpub_manifest_parser/core/parsers.py +++ b/src/webpub_manifest_parser/core/parsers.py @@ -5,7 +5,7 @@ import jsonschema from dateutil import parser as datetime_parser -from jsonschema import FormatError +from jsonschema.exceptions import FormatError from uritemplate import URITemplate from webpub_manifest_parser.errors import BaseError diff --git a/src/webpub_manifest_parser/odl/ast.py b/src/webpub_manifest_parser/odl/ast.py index b4f0777..5ad709e 100644 --- a/src/webpub_manifest_parser/odl/ast.py +++ b/src/webpub_manifest_parser/odl/ast.py @@ -16,7 +16,12 @@ URIProperty, ) from webpub_manifest_parser.odl.registry import ODLCollectionRolesRegistry -from webpub_manifest_parser.opds2.ast import OPDS2Feed, OPDS2Price, OPDS2Publication +from webpub_manifest_parser.opds2.ast import ( + OPDS2AvailabilityInformation, + OPDS2Feed, + OPDS2Price, + OPDS2Publication, +) from webpub_manifest_parser.opds2.registry import OPDS2CollectionRolesRegistry from webpub_manifest_parser.utils import is_string @@ -62,6 +67,9 @@ class ODLLicenseMetadata(Node): nested_type=OPDS2Price, ) source = URIProperty("source", required=False) + availability = TypeProperty( + "availability", required=False, nested_type=OPDS2AvailabilityInformation + ) def __init__(self, identifier=None, formats=None, created=None): """Initialize a new instance of ODLLicenseMetadata class. diff --git a/src/webpub_manifest_parser/opds2/ast.py b/src/webpub_manifest_parser/opds2/ast.py index 3d30801..2d76318 100644 --- a/src/webpub_manifest_parser/opds2/ast.py +++ b/src/webpub_manifest_parser/opds2/ast.py @@ -21,6 +21,7 @@ ) from webpub_manifest_parser.core.properties import ( ArrayProperty, + BooleanProperty, DateOrTimeProperty, DateTimeProperty, EnumProperty, @@ -276,6 +277,8 @@ class OPDS2AvailabilityInformation(Node): ) since = DateOrTimeProperty("since", required=False) until = DateOrTimeProperty("until", required=False) + reason = URIProperty("reason", required=False) + detail = StringProperty("detail", required=False) class OPDS2LinkProperties(LinkProperties): @@ -345,21 +348,36 @@ def __init__( self.number_of_items = number_of_items +class OPDS2PublicationMetadata(PresentationMetadata): + # OPDS2 Removal proposed property. See here for more detail: + # https://github.com/opds-community/drafts/discussions/63 + availability = TypeProperty( + "availability", required=False, nested_type=OPDS2AvailabilityInformation + ) + # Palace OPDS extension to indicate that the publication supports time tracking + time_tracking = BooleanProperty( + "http://palaceproject.io/terms/timeTracking", False, default_value=False + ) + + class OPDS2Publication(Collection): """OPDS 2.0 publication.""" images = CompactCollectionProperty( "images", required=True, role=OPDS2CollectionRolesRegistry.IMAGES ) + metadata = TypeProperty( + key="metadata", required=True, nested_type=OPDS2PublicationMetadata + ) def __init__(self, metadata=None, links=None, images=None): """Initialize a new instance of OPDS2Publication class.""" super().__init__() - if metadata and not isinstance(metadata, PresentationMetadata): + if metadata and not isinstance(metadata, OPDS2PublicationMetadata): raise ValueError( "Argument 'metadata' must be an instance of {}".format( - PresentationMetadata + OPDS2PublicationMetadata ) ) if links and not isinstance(links, LinkList): diff --git a/tests/files/odl/feed.json b/tests/files/odl/feed.json index 0105896..e222538 100644 --- a/tests/files/odl/feed.json +++ b/tests/files/odl/feed.json @@ -29,7 +29,10 @@ "name": "Juvenile Fiction", "links": [] } - ] + ], + "availability": { + "state": "available" + } }, "links": [ { @@ -81,6 +84,12 @@ "copy": false, "print": false, "tts": false + }, + "availability": { + "state": "unavailable", + "until": "2000-05-04T03:02:01Z", + "detail": "a detailed reason", + "reason": "https://registry.opds.io/reason#exhausted" } }, "links": [ diff --git a/tests/files/opds2/feed.json b/tests/files/opds2/feed.json index b9feb0b..6e41544 100644 --- a/tests/files/opds2/feed.json +++ b/tests/files/opds2/feed.json @@ -23,6 +23,13 @@ "urn:isbn:978-3-16-148410-0" ], "language": "en", + "availability": { + "state": "unavailable", + "reason": "https://registry.opds.io/reason#removed", + "detail": "This publication is no longer available in your subscription", + "since": "2019-09-29T01:03:02Z" + }, + "http://palaceproject.io/terms/timeTracking": true, "published": "2015-09-29T00:00:00Z", "modified": "2015-09-29T17:00:00Z", "subject": [ @@ -97,7 +104,10 @@ "href": "http://montreal.pretnumerique.ca/v1/media/EDEN656014-9782404012711-epub/loans_activation.opds2?_resource_id=5f775a8c2357946dbe59f911", "properties": { "availability": { - "state": "available" + "state": "available", + "until": "2019-09-29T01:03:02Z", + "detail": "a detailed reason", + "reason": "http://terms.example.org/available" }, "indirectAcquisition": [ { diff --git a/tests/webpub_manifest_parser/core/test_analyzer.py b/tests/webpub_manifest_parser/core/test_analyzer.py index 5729fc7..afdbb89 100644 --- a/tests/webpub_manifest_parser/core/test_analyzer.py +++ b/tests/webpub_manifest_parser/core/test_analyzer.py @@ -29,6 +29,7 @@ OPDS2Group, OPDS2Navigation, OPDS2Publication, + OPDS2PublicationMetadata, ) from webpub_manifest_parser.rwpm import ( RWPMCollectionRolesRegistry, @@ -136,7 +137,7 @@ def check_analyzer_errors( publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata(title="Publication 1"), links=LinkList( [ Link( @@ -167,7 +168,7 @@ def check_analyzer_errors( publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata(title="Publication 1.1"), + metadata=OPDS2PublicationMetadata(title="Publication 1.1"), links=LinkList( [ Link( @@ -199,7 +200,7 @@ def check_analyzer_errors( publications=CollectionList( [ ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata(title="Publication 1"), links=LinkList([Link(href="http://example.com")]), licenses=CollectionList( [ @@ -215,7 +216,7 @@ def check_analyzer_errors( ), ), ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata(title="Publication 1"), links=LinkList([Link(href="http://example.com")]), licenses=CollectionList( [ diff --git a/tests/webpub_manifest_parser/odl/test_parser.py b/tests/webpub_manifest_parser/odl/test_parser.py index 044ea54..22eb781 100644 --- a/tests/webpub_manifest_parser/odl/test_parser.py +++ b/tests/webpub_manifest_parser/odl/test_parser.py @@ -2,11 +2,16 @@ import os from unittest import TestCase -from dateutil.tz import tzoffset +from dateutil.tz import tzoffset, tzutc from webpub_manifest_parser.core import ManifestParserResult from webpub_manifest_parser.odl import ODLFeedParserFactory -from webpub_manifest_parser.opds2.ast import OPDS2FeedMetadata +from webpub_manifest_parser.opds2.ast import ( + OPDS2AvailabilityInformation, + OPDS2AvailabilityType, + OPDS2FeedMetadata, + OPDS2PublicationMetadata, +) from webpub_manifest_parser.opds2.registry import OPDS2LinkRelationsRegistry from webpub_manifest_parser.utils import first_or_default @@ -34,6 +39,15 @@ def test(self): self.assertEqual(1, len(feed.publications)) [publication] = feed.publications + self.assertIsInstance(publication.metadata, OPDS2PublicationMetadata) + self.assertIsInstance( + publication.metadata.availability, OPDS2AvailabilityInformation + ) + self.assertEqual( + OPDS2AvailabilityType.AVAILABLE.value, + publication.metadata.availability.state, + ) + self.assertEqual(1, len(publication.licenses)) [license] = publication.licenses @@ -70,6 +84,22 @@ def test(self): self.assertEqual(False, license.metadata.protection.print_allowed) self.assertEqual(False, license.metadata.protection.tts_allowed) + self.assertIsInstance( + license.metadata.availability, OPDS2AvailabilityInformation + ) + self.assertEqual( + OPDS2AvailabilityType.UNAVAILABLE.value, license.metadata.availability.state + ) + self.assertEqual( + datetime.datetime(2000, 5, 4, 3, 2, 1, tzinfo=tzutc()), + license.metadata.availability.until, + ) + self.assertEqual("a detailed reason", license.metadata.availability.detail) + self.assertEqual( + "https://registry.opds.io/reason#exhausted", + license.metadata.availability.reason, + ) + self.assertEqual(2, len(license.links)) borrow_link = first_or_default( license.links.get_by_rel(OPDS2LinkRelationsRegistry.BORROW.key) diff --git a/tests/webpub_manifest_parser/odl/test_semantic.py b/tests/webpub_manifest_parser/odl/test_semantic.py index db53a72..49afe8c 100644 --- a/tests/webpub_manifest_parser/odl/test_semantic.py +++ b/tests/webpub_manifest_parser/odl/test_semantic.py @@ -3,12 +3,7 @@ from parameterized import parameterized from tests.webpub_manifest_parser.core.test_analyzer import AnalyzerTest -from webpub_manifest_parser.core.ast import ( - CollectionList, - Link, - LinkList, - PresentationMetadata, -) +from webpub_manifest_parser.core.ast import CollectionList, Link, LinkList from webpub_manifest_parser.core.registry import LinkRelationsRegistry from webpub_manifest_parser.core.semantic import ( MANIFEST_MISSING_SELF_LINK_ERROR, @@ -40,6 +35,7 @@ OPDS2FeedMetadata, OPDS2Group, OPDS2Navigation, + OPDS2PublicationMetadata, ) @@ -141,7 +137,9 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata( + title="Publication 1" + ), licenses=CollectionList(), ) ] @@ -150,7 +148,7 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): [ ODL_PUBLICATION_MUST_CONTAIN_EITHER_LICENSES_OR_OA_ACQUISITION_LINK_ERROR( node=ODLPublication( - metadata=PresentationMetadata(title="Publication 1") + metadata=OPDS2PublicationMetadata(title="Publication 1") ), node_property=None, ) @@ -171,7 +169,9 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata( + title="Publication 1" + ), links=LinkList([Link(href="http://example.com")]), ) ] @@ -180,7 +180,7 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): [ ODL_PUBLICATION_MUST_CONTAIN_EITHER_LICENSES_OR_OA_ACQUISITION_LINK_ERROR( node=ODLPublication( - metadata=PresentationMetadata(title="Publication 1") + metadata=OPDS2PublicationMetadata(title="Publication 1") ), node_property=None, ) @@ -201,7 +201,9 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata( + title="Publication 1" + ), links=LinkList( [ Link( @@ -233,7 +235,9 @@ class ODLSemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ ODLPublication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata( + title="Publication 1" + ), links=LinkList([Link(href="http://example.com")]), licenses=CollectionList( [ diff --git a/tests/webpub_manifest_parser/opds2/test_parser.py b/tests/webpub_manifest_parser/opds2/test_parser.py index 1473223..c90197b 100644 --- a/tests/webpub_manifest_parser/opds2/test_parser.py +++ b/tests/webpub_manifest_parser/opds2/test_parser.py @@ -15,10 +15,12 @@ from webpub_manifest_parser.opds2 import OPDS2FeedParserFactory from webpub_manifest_parser.opds2.ast import ( OPDS2AcquisitionObject, + OPDS2AvailabilityInformation, OPDS2AvailabilityType, OPDS2Feed, OPDS2FeedMetadata, OPDS2LinkProperties, + OPDS2PublicationMetadata, ) from webpub_manifest_parser.opds2.registry import ( OPDS2LinkRelationsRegistry, @@ -60,7 +62,7 @@ def test(self): self.assertEqual(2, len(feed.publications)) publication = feed.publications[0] - self.assertIsInstance(publication.metadata, PresentationMetadata) + self.assertIsInstance(publication.metadata, OPDS2PublicationMetadata) self.assertEqual("http://schema.org/Book", publication.metadata.type) self.assertEqual("Moby-Dick", publication.metadata.title) self.assertEqual( @@ -77,6 +79,26 @@ def test(self): datetime.datetime(2015, 9, 29, 17, 0, tzinfo=tzutc()), publication.metadata.modified, ) + self.assertIsInstance( + publication.metadata.availability, OPDS2AvailabilityInformation + ) + self.assertEqual( + publication.metadata.availability.state, + OPDS2AvailabilityType.UNAVAILABLE.value, + ) + self.assertEqual( + publication.metadata.availability.since, + datetime.datetime(2019, 9, 29, 1, 3, 2, tzinfo=tzutc()), + ) + self.assertEqual( + publication.metadata.availability.detail, + "This publication is no longer available in your subscription", + ) + self.assertEqual( + publication.metadata.availability.reason, + "https://registry.opds.io/reason#removed", + ) + self.assertIs(publication.metadata.time_tracking, True) self.assertIsInstance(publication.links, list) self.assertEqual(len(publication.links), 2) @@ -162,6 +184,9 @@ def test(self): publication.metadata.modified, ) + self.assertIs(publication.metadata.availability, None) + self.assertIs(publication.metadata.time_tracking, False) + self.assertIsInstance(publication.links, list) publication_acquisition_link = first_or_default( @@ -178,9 +203,15 @@ def test(self): link_properties = publication_acquisition_link.properties self.assertIsInstance(link_properties, OPDS2LinkProperties) + availability = link_properties.availability + self.assertIsInstance(availability, OPDS2AvailabilityInformation) + self.assertEqual(OPDS2AvailabilityType.AVAILABLE.value, availability.state) self.assertEqual( - OPDS2AvailabilityType.AVAILABLE.value, link_properties.availability.state + datetime.datetime(2019, 9, 29, 1, 3, 2, tzinfo=tzutc()), + availability.until, ) + self.assertEqual("a detailed reason", availability.detail) + self.assertEqual("http://terms.example.org/available", availability.reason) self.assertEqual(2, len(link_properties.indirect_acquisition)) diff --git a/tests/webpub_manifest_parser/opds2/test_semantic.py b/tests/webpub_manifest_parser/opds2/test_semantic.py index 258bb91..d253c93 100644 --- a/tests/webpub_manifest_parser/opds2/test_semantic.py +++ b/tests/webpub_manifest_parser/opds2/test_semantic.py @@ -3,12 +3,7 @@ from parameterized import parameterized from tests.webpub_manifest_parser.core.test_analyzer import AnalyzerTest -from webpub_manifest_parser.core.ast import ( - CollectionList, - Link, - LinkList, - PresentationMetadata, -) +from webpub_manifest_parser.core.ast import CollectionList, Link, LinkList from webpub_manifest_parser.core.semantic import SemanticAnalyzerError from webpub_manifest_parser.opds2.ast import ( OPDS2Feed, @@ -16,6 +11,7 @@ OPDS2Group, OPDS2Navigation, OPDS2Publication, + OPDS2PublicationMetadata, ) from webpub_manifest_parser.opds2.registry import ( OPDS2CollectionRolesRegistry, @@ -91,7 +87,9 @@ class OPDS2SemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata( + title="Publication 1" + ), links=LinkList([Link(href="http://example.com")]), ) ] @@ -127,7 +125,7 @@ class OPDS2SemanticAnalyzerTest(AnalyzerTest): publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata( + metadata=OPDS2PublicationMetadata( title="Publication 1" ), links=LinkList( @@ -192,7 +190,7 @@ def test_semantic_analyzer_does_correctly_processes_valid_ast(self): publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata(title="Publication 1"), + metadata=OPDS2PublicationMetadata(title="Publication 1"), links=LinkList( [ Link( @@ -223,7 +221,7 @@ def test_semantic_analyzer_does_correctly_processes_valid_ast(self): publications=CollectionList( [ OPDS2Publication( - metadata=PresentationMetadata( + metadata=OPDS2PublicationMetadata( title="Publication 1.1" ), links=LinkList(