Repository: iiif-prezi/iiif-prezi3 (language: python)

Issue:
Set HWD from IIIF
<!-- Add a short name of the proposed helper method in the Title above -->

## Description
Retrieve the `info.json` for a resource, and add height, width, and duration properties to a Canvas or Content Resource object, allowing nulls

## Function name / Parameters / Return
<!-- Suggest a name for the helper's function, the parameters it should accept, and what it should return (if anything) -->
Function name: `.set_hwd_from_iiif(url)`

Parameters: `url` - the IIIF Image API URL for the resource

Returns: `None`

## Objects
<!-- Which objects should this helper apply to? -->
- [ ] Base Object (all Prezi3 Classes)
- [ ] Collection
- [ ] Manifest
- [x] Canvas
- [ ] Range
- [ ] AnnotationCollection
- [ ] AnnotationPage
- [ ] Annotation
- [x] Content Resource - Image
- [x] Content Resource - Video
- [ ] Content Resource - Text
- [ ] Service
- [ ] Provider
- [ ] Other - please list below
<!-- Add other Objects/Classes here -->

## Considerations
<!-- Any extra considerations for the behavior of this helper -->
This will introduce a new dependency for retrieving the `info.json` file, probably [requests](https://requests.readthedocs.io/en/latest/). Please discuss before adding.

The URL provided by the user might already be the full path to the `info.json` file, so the helper should check this and not add it if unnecessary,

For Canvas objects, if `height` is present, then `width` must also be set, and vice-versa (https://iiif.io/api/presentation/3.0/#height / https://iiif.io/api/presentation/3.0/#width), Therefore the helper must be type aware, and raise an error if one is set without the other when the target is a Canvas object

Since this helper will be making a request to an external HTTP(S) resource, it should be prepared to gracefully handle errors and 404s etc, raising them to the main library as appropriate.

Add Set_HWD and related tests
This add pull request adds the method Set_HWD to Canvases and ResourceItem and tests for checking it works correctly.

https://github.com/iiif-prezi/iiif-prezi3/issues/54


Discussion / hints:
It occurs to me that this helper _could_ call the Set HWD helper #(54) once it has established the values, to minimise code duplication a little. This is something we could add after if we wanted, so not a hard requirement for the hackathon.
@digitaldogsbody that seems logical, should I work on #54 first? (never mind, I see someone else has grabbed it)


Fix patch:
diff --git a/iiif_prezi3/helpers/__init__.py b/iiif_prezi3/helpers/__init__.py
index 5298588..b3ddea7 100644
--- a/iiif_prezi3/helpers/__init__.py
+++ b/iiif_prezi3/helpers/__init__.py
@@ -5,8 +5,9 @@
 from .auto_fields import *  # noqa: F401,F403
 from .canvas_helpers import AddImageToCanvas  # noqa: F401
 from .canvas_sizes import MaxHelper, MinHelper  # noqa: F401
+from .set_hwd_from_iiif import SetHwdFromIIIF  # noqa: F401
 from .make_collection import MakeCollection # noqa: F401
 from .make_manifest import MakeManifest # noqa: F401
 from .add_range import AddRange # noqa: F401
 from .set_hwd import SetHwd  # noqa: F401
-from .to_reference import ToReference  # noqa: F401
+from .to_reference import ToReference  # noqa: F401
\ No newline at end of file
diff --git a/iiif_prezi3/helpers/set_hwd_from_iiif.py b/iiif_prezi3/helpers/set_hwd_from_iiif.py
new file mode 100644
index 0000000..0b8ab46
--- /dev/null
+++ b/iiif_prezi3/helpers/set_hwd_from_iiif.py
@@ -0,0 +1,39 @@
+import requests
+
+from ..loader import monkeypatch_schema
+from ..skeleton import Canvas
+
+
+class SetHwdFromIIIF:
+    # should probably be added to canvas helpers
+
+    def set_hwd_from_iiif(self, url):
+        """Set height and width on a Canvas object.
+
+        Requests IIIF Image information remotely for an
+        image resource and sets resulting height and width.
+
+        Args:
+            url (str): An HTTP URL for the IIIF image endpoint.
+        """
+        # resource url may or may not end with info.json;
+        # add if not present
+        if not url.endswith("info.json"):
+            url = f"{ url.rstrip('/') }/info.json"
+
+        response = requests.get(url)
+        # if response is not 200, raise exception
+        if response.status_code != requests.codes.ok:
+            response.raise_for_status()
+        # if response is not valid json, request will raise
+        # requests.exceptions.JSONDecodeError
+        # — handle or document and let calling code handle?
+        resource_info = response.json()
+        self.set_hwd(resource_info.get("height"), resource_info.get("width"))
+
+    def set_hwd(self, height=None, width=None, duration=None):
+        # placeholder for other helper
+        pass
+
+
+monkeypatch_schema(Canvas, [SetHwdFromIIIF])
diff --git a/setup.py b/setup.py
index 75d7a6e..6404432 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,8 @@
     VERSION = "local_test_version"
 
 REQUIREMENTS = [
-    "pydantic"
+    "pydantic",
+    "requests",
 ]
 
 DOCS_REQUIREMENTS = [
diff --git a/tox.ini b/tox.ini
index 883e582..dafb12c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,6 +12,7 @@ python =
 [testenv]
 deps =
 	pydantic
+  requests
 skip_install = True
 commands =
 	python -m unittest discover -s tests


Test patch:
diff --git a/tests/test_helpers/__init__.py b/tests/test_helpers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_helpers/test_set_hwd_from_iiif.py b/tests/test_helpers/test_set_hwd_from_iiif.py
new file mode 100644
index 0000000..1bca0e5
--- /dev/null
+++ b/tests/test_helpers/test_set_hwd_from_iiif.py
@@ -0,0 +1,58 @@
+import unittest
+from unittest.mock import Mock, patch
+
+import requests
+
+from iiif_prezi3 import Canvas
+
+
+class SetHwdFromIIIFTests(unittest.TestCase):
+
+    def setUp(self):
+        self.canvas = Canvas(id='http://iiif.example.org/prezi/Canvas/0')
+
+    @patch('iiif_prezi3.helpers.set_hwd_from_iiif.requests.get')
+    def test_set_hwd_from_iiif(self, mockrequest_get):
+        image_id = 'http://iiif.example.org/images/1234abcd'
+        image_info_url = f'{image_id}/info.json'
+        # image api url without info.json
+        self.canvas.set_hwd_from_iiif(image_id)
+        mockrequest_get.assert_called_with(image_info_url)
+
+        # image api url with info.json
+        mockrequest_get.reset_mock()
+        self.canvas.set_hwd_from_iiif(image_id)
+        mockrequest_get.assert_called_with(image_info_url)
+
+        # image url with trailing slash
+        mockrequest_get.reset_mock()
+        self.canvas.set_hwd_from_iiif(image_id + "/")
+        mockrequest_get.assert_called_with(image_info_url)
+
+        # non-200 response
+        mockresponse = Mock(status_code=404)
+        mockrequest_get.return_value = mockresponse
+        self.canvas.set_hwd_from_iiif(image_id)
+        # non-200 should raise exception for status
+        mockresponse.raise_for_status.assert_called_once()
+
+        # 200 response but not valid json should raise exception
+        mockresponse.status_code = 200
+        mockresponse.json.side_effect = requests.exceptions.JSONDecodeError
+        with self.assertRaises(requests.exceptions.JSONDecodeError):
+            self.canvas.set_hwd_from_iiif(image_id)
+
+        # successful response with dimensions
+        mockresponse.json.side_effect = None
+        # set mock to return minimal image api response
+        mockresponse.json.return_value = {
+            "@context": "http://iiif.io/api/image/2/context.json",
+            "@id": image_id,
+            "protocol": "http://iiif.io/api/image",
+            "width": 4821,
+            "height": 6608
+        }
+        # patch the method this helper calls so we can inspect
+        with patch.object(Canvas, 'set_hwd') as mock_set_hwd:
+            self.canvas.set_hwd_from_iiif(image_id)
+            mock_set_hwd.assert_called_with(6608, 4821)