diff --git a/iiif_prezi3/base.py b/iiif_prezi3/base.py index e6d8c41..3648df9 100644 --- a/iiif_prezi3/base.py +++ b/iiif_prezi3/base.py @@ -1,7 +1,6 @@ import json -from pydantic import BaseModel - +from pydantic import AnyUrl, BaseModel class Base(BaseModel): @@ -10,16 +9,18 @@ class Config: validate_all = True copy_on_model_validation = False smart_union = True + # Allow us to use the field name like service.id rather than service.@id + allow_population_by_field_name = True def __getattribute__(self, prop): val = super(Base, self).__getattribute__(prop) # __root__ is a custom pydantic thing if hasattr(val, '__root__'): - if type(val.__root__) in [dict, list, float, int]: - return val.__root__ - else: + if type(val.__root__) in [AnyUrl]: # cast it to a string return str(val.__root__) + else: + return val.__root__ else: return val @@ -55,6 +56,7 @@ def jsonld(self, **kwargs): excluded_args = ["exclude_unset", "exclude_defaults", "exclude_none", "by_alias"] pydantic_args = ["include", "exclude", "encoder"] dict_kwargs = dict([(arg, kwargs[arg]) for arg in kwargs.keys() if arg in pydantic_args]) + json_kwargs = dict([(arg, kwargs[arg]) for arg in kwargs.keys() if arg not in pydantic_args + excluded_args]) return json.dumps({"@context": "http://iiif.io/api/presentation/3/context.json", **self.dict(exclude_unset=False, exclude_defaults=False, exclude_none=True, by_alias=True, **dict_kwargs)}, **json_kwargs) @@ -62,5 +64,6 @@ def jsonld(self, **kwargs): def jsonld_dict(self, **kwargs): pydantic_args = ["include", "exclude", "encoder"] dict_kwargs = dict([(arg, kwargs[arg]) for arg in kwargs.keys() if arg in pydantic_args]) + return {"@context": "http://iiif.io/api/presentation/3/context.json", **self.dict(exclude_unset=False, exclude_defaults=False, exclude_none=True, by_alias=True, **dict_kwargs)} diff --git a/iiif_prezi3/helpers/__init__.py b/iiif_prezi3/helpers/__init__.py index 13aa80d..6221d09 100644 --- a/iiif_prezi3/helpers/__init__.py +++ b/iiif_prezi3/helpers/__init__.py @@ -6,6 +6,7 @@ from .auto_fields import * # noqa: F401,F403 from .canvas_helpers import AddImageToCanvas # noqa: F401 from .canvas_sizes import MaxHelper, MinHelper # noqa: F401 +from .create_canvas_from_iiif import CreateCanvasFromIIIF # noqa: F401 from .make_canvas import MakeCanvas # noqa: F401 from .make_collection import MakeCollection # noqa: F401 from .make_manifest import MakeManifest # noqa: F401 diff --git a/iiif_prezi3/helpers/create_canvas_from_iiif.py b/iiif_prezi3/helpers/create_canvas_from_iiif.py new file mode 100644 index 0000000..a270176 --- /dev/null +++ b/iiif_prezi3/helpers/create_canvas_from_iiif.py @@ -0,0 +1,59 @@ + +from ..loader import monkeypatch_schema +from ..skeleton import (Annotation, AnnotationPage, Canvas, Manifest, + ResourceItem, ServiceItem, ServiceItem1) + + +class CreateCanvasFromIIIF: + # should probably be added to canvas helpers + + def create_canvas_from_iiif(self, url, **kwargs): + """Create a canvas from a IIIF Image URL. + + Creates a canvas from a IIIF Image service passing any + kwargs to the Canvas. Returns a Canvas object + + """ + canvas = Canvas(**kwargs) + + body = ResourceItem(id="http://example.com", type="Image") + infoJson = body.set_hwd_from_iiif(url) + + # Will need to handle IIIF 2... + if 'type' not in infoJson: + # Assume v2 + + # V2 profile contains profile URI plus extra features + profile = '' + for item in infoJson['profile']: + if isinstance(item, str): + profile = item + break + + service = ServiceItem1(id=infoJson['@id'], profile=profile, type="ImageService2") + body.service = [service] + body.id = f'{infoJson["@id"]}/full/full/0/default.jpg' + body.format = "image/jpeg" + else: + service = ServiceItem(id=infoJson['id'], profile=infoJson['profile'], type=infoJson['type']) + body.service = [service] + body.id = f'{infoJson["id"]}/full/max/0/default.jpg' + body.format = "image/jpeg" + + annotation = Annotation(motivation='painting', body=body, target=canvas.id) + + annotationPage = AnnotationPage() + annotationPage.add_item(annotation) + + canvas.add_item(annotationPage) + canvas.set_hwd(infoJson['height'], infoJson['width']) + + return canvas + + def make_canvas_from_iiif(self, url, **kwargs): + canvas = self.create_canvas_from_IIIF(url, **kwargs) + + self.add_item(canvas) + + +monkeypatch_schema(Manifest, [CreateCanvasFromIIIF]) diff --git a/iiif_prezi3/helpers/set_hwd_from_iiif.py b/iiif_prezi3/helpers/set_hwd_from_iiif.py index 57bd11f..b1ab792 100644 --- a/iiif_prezi3/helpers/set_hwd_from_iiif.py +++ b/iiif_prezi3/helpers/set_hwd_from_iiif.py @@ -1,7 +1,7 @@ import requests from ..loader import monkeypatch_schema -from ..skeleton import Canvas +from ..skeleton import Canvas, Resource, ResourceItem class SetHwdFromIIIF: @@ -12,6 +12,7 @@ def set_hwd_from_iiif(self, url): Requests IIIF Image information remotely for an image resource and sets resulting height and width. + This method will return the info.json Args: url (str): An HTTP URL for the IIIF image endpoint. @@ -31,5 +32,7 @@ def set_hwd_from_iiif(self, url): resource_info = response.json() self.set_hwd(resource_info.get("height"), resource_info.get("width")) + return resource_info -monkeypatch_schema(Canvas, [SetHwdFromIIIF]) + +monkeypatch_schema([Canvas, Resource, ResourceItem], SetHwdFromIIIF) diff --git a/iiif_prezi3/skeleton.py b/iiif_prezi3/skeleton.py index a423bc6..e101d5a 100644 --- a/iiif_prezi3/skeleton.py +++ b/iiif_prezi3/skeleton.py @@ -320,8 +320,8 @@ class ServiceItem(Class): class ServiceItem1(Base): - _id: Id = Field(..., alias='@id') - _type: str = Field(..., alias='@type') + id: Id = Field(..., alias='@id') + type: str = Field(..., alias='@type') profile: Optional[str] = None service: Optional[Service] = None diff --git a/tests/test_basic.py b/tests/test_basic.py index e533b8b..39fe65f 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -85,7 +85,8 @@ def testLabel(self): manifest = Manifest(id='http://iiif.example.org/prezi/Manifest/0', type='Manifest', label={'en': ['default label']}) self.assertTrue('en' in manifest.label, 'Manifest seems to be missing English label') - self.assertEqual(manifest.label['en'], ['default label'], 'Unexpected label for manifest') + self.assertEqual(manifest.label['en'][0], 'default label', 'Unexpected label for manifest') + if __name__ == '__main__': diff --git a/tests/test_create_canvas_from_iiif.py b/tests/test_create_canvas_from_iiif.py new file mode 100644 index 0000000..58a638c --- /dev/null +++ b/tests/test_create_canvas_from_iiif.py @@ -0,0 +1,128 @@ +import unittest +from unittest.mock import Mock, patch + +from iiif_prezi3 import Manifest + + +class CreateCanvasFromIIIFTests(unittest.TestCase): + + def setUp(self): + self.manifest = Manifest(label={'en': ['Manifest label']}) + + @patch('iiif_prezi3.helpers.set_hwd_from_iiif.requests.get') + def test_create_canvas_from_iiif_v3(self, mockrequest_get): + image_id = 'https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen' + image_info_url = f'{image_id}/info.json' + + # successful response with dimensions + mockresponse = Mock(status_code=200) + mockrequest_get.return_value = mockresponse + # set mock to return minimal image api response + mockresponse.json.return_value = { + "@context": "http://iiif.io/api/image/3/context.json", + "extraFormats": ["jpg", "png"], + "extraQualities": ["default", "color", "gray"], + "height": 3024, + "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", + "profile": "level1", + "protocol": "http://iiif.io/api/image", + "tiles": [{ + "height": 512, + "scaleFactors": [1, 2, 4], + "width": 512 + }], + "type": "ImageService3", + "width": 4032 + } + + canvas = self.manifest.create_canvas_from_iiif(image_info_url, label={'en': ['Canvas label']}) + + # Check canvas params made it through + self.assertEqual(canvas.label, + {'en': ['Canvas label']}) + + # check canvas dimensions + self.assertEqual(canvas.height, 3024) + self.assertEqual(canvas.width, 4032) + + # Check annotation + annotation = canvas.items[0].items[0] + self.assertEqual(annotation.motivation, "painting") + + # Check resource + resource = annotation.body + + self.assertEqual(resource.id, f'{image_id}/full/max/0/default.jpg') + self.assertEqual(resource.type, 'Image') + self.assertEqual(resource.format, 'image/jpeg') + self.assertEqual(resource.height, 3024) + self.assertEqual(resource.width, 4032) + + # Check service + service = resource.service[0] + + self.assertEqual(service.id, image_id) + self.assertEqual(service.profile, "level1") + self.assertEqual(service.type, "ImageService3") + + @patch('iiif_prezi3.helpers.set_hwd_from_iiif.requests.get') + def test_create_canvas_from_iiif_v2(self, mockrequest_get): + image_id = 'https://iiif.io/api/image/2.1/example/reference/918ecd18c2592080851777620de9bcb5-gottingen' + image_info_url = f'{image_id}/info.json' + + # successful response with dimensions + mockresponse = Mock(status_code=200) + mockrequest_get.return_value = mockresponse + # set mock to return minimal image api response + mockresponse.json.return_value = { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://iiif.io/api/image/2.1/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", + "height": 3024, + "profile": [ + "http://iiif.io/api/image/2/level1.json", + { + "formats": [ "jpg", "png" ], + "qualities": [ "default", "color", "gray" ] + } + ], + "protocol": "http://iiif.io/api/image", + "tiles": [ + { + "height": 512, + "scaleFactors": [ 1, 2, 4 ], + "width": 512 + } + ], + "width": 4032 + } + + canvas = self.manifest.create_canvas_from_iiif(image_info_url, label={'en': ['Canvas label']}) + + # Check canvas params made it through + self.assertEqual(canvas.label, + {'en': ['Canvas label']}) + + # check canvas dimensions + self.assertEqual(canvas.height, 3024) + self.assertEqual(canvas.width, 4032) + + # Check annotation + annotation = canvas.items[0].items[0] + self.assertEqual(annotation.motivation, "painting") + + # Check resource + resource = annotation.body + + self.assertEqual(resource.id, f'{image_id}/full/full/0/default.jpg') + self.assertEqual(resource.type, 'Image') + self.assertEqual(resource.format, 'image/jpeg') + self.assertEqual(resource.height, 3024) + self.assertEqual(resource.width, 4032) + + # Check service + service = resource.service[0] + + self.assertEqual(service.id, image_id) + self.assertEqual(service.profile, "http://iiif.io/api/image/2/level1.json") + self.assertEqual(service.type, "ImageService2") +