From 08bb9009b7e13fdfa79358d1c5d6586e0b83f80b Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Wed, 21 Feb 2018 11:36:42 +0000 Subject: [PATCH 01/27] Add 3.0 with 2.1 behaviors --- iiif/info.py | 23 +- iiif_reference_server.py | 2 +- iiif_testserver.py | 2 +- tests/test_info_3_0.py | 242 +++++++++++ tests/test_info_3_0_auth_services.py | 96 +++++ tests/test_info_common.py | 2 +- tests/test_request_3_0.py | 396 ++++++++++++++++++ tests/test_request_spec_3_0.py | 71 ++++ .../info_json_3_0/info_bad_context.json | 31 ++ .../info_from_spec_section_5_2.json | 16 + .../info_from_spec_section_5_3.json | 18 + .../info_from_spec_section_5_6.json | 58 +++ .../info_json_3_0/info_with_extra.json | 34 ++ 13 files changed, 987 insertions(+), 4 deletions(-) create mode 100644 tests/test_info_3_0.py create mode 100644 tests/test_info_3_0_auth_services.py create mode 100644 tests/test_request_3_0.py create mode 100644 tests/test_request_spec_3_0.py create mode 100644 tests/testdata/info_json_3_0/info_bad_context.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_2.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_3.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_6.json create mode 100644 tests/testdata/info_json_3_0/info_with_extra.json diff --git a/iiif/info.py b/iiif/info.py index 482a9ad..9776521 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -1,7 +1,7 @@ """IIIF Image Information Response. Model for IIIF Image API 'Image Information Response'. -Default version is 2.1 but also supports 2.0, 1.1 and 1.0. +Default version is 2.1 but also supports 3.0, 2.0, 1.1 and 1.0. Philisophy is to migrate this code forward with new versions of the specification but to keep support for all published @@ -143,6 +143,27 @@ def _parse_profile(info, json_data): 'protocol': "http://iiif.io/api/image", 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], + }, + '3.0': { + 'params': + ['identifier', 'protocol', 'width', 'height', + 'profile', 'sizes', 'tiles', 'service', + 'attribution', 'logo', 'license'], + # scale_factors isn't in API but used internally + 'array_params': set( + ['sizes', 'tiles', 'service', 'scale_factors', 'formats', + 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']), + 'complex_params': { + 'sizes': _parse_noop, + 'tiles': _parse_tiles, + 'profile': _parse_profile, + 'service': _parse_service}, + 'context': "http://iiif.io/api/image/3/context.json", + 'compliance_prefix': "http://iiif.io/api/image/3/level", + 'compliance_suffix': ".json", + 'protocol': "http://iiif.io/api/image", + 'required_params': + ['identifier', 'protocol', 'width', 'height', 'profile'], } } diff --git a/iiif_reference_server.py b/iiif_reference_server.py index 450f418..478c286 100755 --- a/iiif_reference_server.py +++ b/iiif_reference_server.py @@ -33,7 +33,7 @@ def get_config(base_dir=''): p.add('--scale-factors', default='auto', help="Set of tile scale factors or 'auto' to calculate for each image " "such that there are tiles up to the full image") - p.add('--api-versions', default='1.0,1.1,2.0,2.1', + p.add('--api-versions', default='1.0,1.1,2.0,2.1,3.0', help="Set of API versions to support") args = p.parse_args() diff --git a/iiif_testserver.py b/iiif_testserver.py index 92ee5bc..e92df27 100755 --- a/iiif_testserver.py +++ b/iiif_testserver.py @@ -36,7 +36,7 @@ def get_config(base_dir=''): p.add('--scale-factors', default='auto', help="Set of tile scale factors or 'auto' to calculate for each image " "such that there are tiles up to the full image") - p.add('--api-versions', default='1.0,1.1,2.0,2.1', + p.add('--api-versions', default='1.0,1.1,2.0,2.1,3.0', help="Set of API versions to support") p.add('--manipulators', default='pil', help="Set of manipuators to instantiate. May be dummy,netpbm,pil " diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py new file mode 100644 index 0000000..7b30a82 --- /dev/null +++ b/tests/test_info_3_0.py @@ -0,0 +1,242 @@ +"""Test code for iiif/info.py for Image API v3.0.""" +import unittest +from .testlib.assert_json_equal_mixin import AssertJSONEqual +import json +from iiif.info import IIIFInfo + + +class TestAll(unittest.TestCase, AssertJSONEqual): + """Tests.""" + + def test01_minmal(self): + """Trivial JSON test.""" + # ?? should this empty case raise and error instead? + ir = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + self.assertJSONEqual(ir.as_json(validate=False), + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image"\n}') + ir.width = 100 + ir.height = 200 + self.assertJSONEqual(ir.as_json(), + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "height": 200, \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", \n "width": 100\n}') + + def test04_conf(self): + """Tile parameter configuration.""" + conf = {'tiles': [{'width': 999, 'scaleFactors': [9, 8, 7]}]} + i = IIIFInfo(api_version='3.0', conf=conf) + self.assertEqual(i.tiles[0]['width'], 999) + self.assertEqual(i.tiles[0]['scaleFactors'], [9, 8, 7]) + # 1.1 style values + self.assertEqual(i.tile_width, 999) + self.assertEqual(i.scale_factors, [9, 8, 7]) + + def test05_level_and_profile(self): + """Test level and profile setting.""" + i = IIIFInfo(api_version='3.0') + i.level = 0 + self.assertEqual(i.level, 0) + self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level0.json") + i.level = 2 + self.assertEqual(i.level, 2) + self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json") + # Set via compliance + i.compliance = "http://iiif.io/api/image/3/level1.json" + self.assertEqual(i.level, 1) + # Set via profile + i.profile = ["http://iiif.io/api/image/3/level1.json"] + self.assertEqual(i.level, 1) + # Set new via compliance + i = IIIFInfo(api_version='3.0') + i.compliance = "http://iiif.io/api/image/3/level1.json" + self.assertEqual(i.level, 1) + + def test06_validate(self): + """Test validate method.""" + i = IIIFInfo(api_version='3.0') + self.assertRaises(Exception, i.validate, ()) + i = IIIFInfo(identifier='a') + self.assertRaises(Exception, i.validate, ()) + i = IIIFInfo(identifier='a', width=1, height=2) + self.assertTrue(i.validate()) + + def test10_read_examples_from_spec(self): + """Test reading of examples from spec.""" + # Section 5.2, full example + i = IIIFInfo(api_version='3.0') + fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_2.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + self.assertEqual(i.sizes, [{"width": 150, "height": 100}, + {"width": 600, "height": 400}, + {"width": 3000, "height": 2000}]) + self.assertEqual(i.tiles, [{"width": 512, + "scaleFactors": [1, 2, 4, 8, 16]}]) + self.assertEqual(i.profile, + ["http://iiif.io/api/image/3/level2.json"]) + # extracted information + self.assertEqual(i.compliance, + "http://iiif.io/api/image/3/level2.json") + # and 1.1 style tile properties + self.assertEqual(i.tile_width, 512) + self.assertEqual(i.tile_height, 512) + self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16]) + + # Section 5.3, full example + i = IIIFInfo(api_version='3.0') + fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_3.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 4000) + self.assertEqual(i.height, 3000) + self.assertEqual( + i.profile, + ["http://iiif.io/api/image/3/level2.json", + {"formats": ["gif", "pdf"], + "maxWidth": 2000, + "qualities": ["color", "gray"], + "supports": ["canonicalLinkHeader", "rotationArbitrary", + "profileLinkHeader", "http://example.com/feature/"]}]) + # extracted information + self.assertEqual(i.compliance, + "http://iiif.io/api/image/3/level2.json") + + # Section 5.6, full example + i = IIIFInfo(api_version='3.0') + fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + self.assertEqual(i.sizes, [{"width": 150, "height": 100}, + {"width": 600, "height": 400}, + {"width": 3000, "height": 2000}]) + + def test11_read_example_with_extra(self): + """Test read of exampe with extra info.""" + i = IIIFInfo(api_version='3.0') + fh = open('tests/testdata/info_json_3_0/info_with_extra.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + self.assertEqual( + i.tiles, [{"width": 512, "scaleFactors": [1, 2, 4, 8, 16]}]) + # and should have 1.1-like params too + self.assertEqual(i.tile_width, 512) + self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16]) + self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json") + + def test12_read_unknown_context(self): + """Test bad/unknown context.""" + i = IIIFInfo(api_version='3.0') + fh = open('tests/testdata/info_json_3_0/info_bad_context.json') + self.assertRaises(Exception, i.read, fh) + + def test20_write_example_in_spec(self): + """Create example info.json in spec.""" + i = IIIFInfo( + api_version='3.0', + id="http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + # "protocol" : "http://iiif.io/api/image", + width=6000, + height=4000, + sizes=[ + {"width": 150, "height": 100}, + {"width": 600, "height": 400}, + {"width": 3000, "height": 2000}], + tiles=[ + {"width": 512, "scaleFactors": [1, 2, 4]}, + {"width": 1024, "height": 2048, "scaleFactors": [8, 16]}], + attribution=[ + {"@value": "Provided by Example Organization", + "@language": "en"}, + {"@value": "Darparwyd gan Enghraifft Sefydliad", + "@language": "cy"}], + logo={"@id": "http://example.org/image-service/logo/full/200,/0/default.png", + "service": + {"@context": "http://iiif.io/api/image/3/context.json", + "@id": "http://example.org/image-service/logo", + "profile": "http://iiif.io/api/image/3/level2.json"}}, + license=[ + "http://example.org/rights/license1.html", + "https://creativecommons.org/licenses/by/4.0/"], + profile=["http://iiif.io/api/image/3/level2.json"], + formats=["gif", "pdf"], + qualities=["color", "gray"], + supports=["canonicalLinkHeader", "rotationArbitrary", + "profileLinkHeader", "http://example.com/feature/"], + service=[ + {"@context": "http://iiif.io/api/annex/service/physdim/1/context.json", + "profile": "http://iiif.io/api/annex/service/physdim", + "physicalScale": 0.0025, + "physicalUnits": "in"}, + {"@context": "http://geojson.org/contexts/geojson-base.jsonld", + "@id": "http://www.example.org/geojson/paris.json"}] + ) + reparsed_json = json.loads(i.as_json()) + example_json = json.load( + open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json')) + self.maxDiff = 4000 + self.assertEqual(reparsed_json, example_json) + + def test21_write_profile(self): + """Test writing of profile information.""" + i = IIIFInfo( + api_version='3.0', + id="http://example.org/svc/a", width=1, height=2, + profile=['pfl'], formats=["fmt1", "fmt2"]) + j = json.loads(i.as_json()) + self.assertEqual(len(j['profile']), 2) + self.assertEqual(j['profile'][0], 'pfl') + self.assertEqual(j['profile'][1], {'formats': ['fmt1', 'fmt2']}) + i = IIIFInfo( + api_version='3.0', + id="http://example.org/svc/a", width=1, height=2, + profile=['pfl'], qualities=None) + j = json.loads(i.as_json()) + self.assertEqual(len(j['profile']), 1) + self.assertEqual(j['profile'][0], 'pfl') + i = IIIFInfo( + api_version='3.0', + id="http://example.org/svc/a", width=1, height=2, + profile=['pfl'], qualities=['q1', 'q2', 'q0']) + j = json.loads(i.as_json()) + self.assertEqual(len(j['profile']), 2) + self.assertEqual(j['profile'][0], 'pfl') + self.assertEqual(j['profile'][1], {'qualities': ['q1', 'q2', 'q0']}) + i = IIIFInfo( + api_version='3.0', + id="http://example.org/svc/a", width=1, height=2, + profile=['pfl'], supports=['a', 'b']) + j = json.loads(i.as_json()) + self.assertEqual(len(j['profile']), 2) + self.assertEqual(j['profile'][0], 'pfl') + self.assertEqual(j['profile'][1], {'supports': ['a', 'b']}) + i = IIIFInfo( + api_version='3.0', + id="http://example.org/svc/a", width=1, height=2, + profile=['pfl'], formats=["fmt1", "fmt2"], + qualities=['q1', 'q2', 'q0'], supports=['a', 'b']) + j = json.loads(i.as_json()) + self.assertEqual(len(j['profile']), 2) + self.assertEqual(j['profile'][0], 'pfl') + self.assertEqual(j['profile'][1]['formats'], ['fmt1', 'fmt2']) + self.assertEqual(j['profile'][1]['qualities'], ['q1', 'q2', 'q0']) + self.assertEqual(j['profile'][1]['supports'], ['a', 'b']) diff --git a/tests/test_info_3_0_auth_services.py b/tests/test_info_3_0_auth_services.py new file mode 100644 index 0000000..44c5521 --- /dev/null +++ b/tests/test_info_3_0_auth_services.py @@ -0,0 +1,96 @@ +"""Test code for iiif/info.py for Image API v3.0 auth service descriptions. + +See: https://github.com/IIIF/iiif.io/blob/image-auth/source/api/image/3.0/authentication.md +""" +import unittest +from .testlib.assert_json_equal_mixin import AssertJSONEqual +import json +from iiif.info import IIIFInfo +from iiif.auth import IIIFAuth + + +class TestAll(unittest.TestCase, AssertJSONEqual): + """Tests.""" + + def test01_empty_auth_defined(self): + """Test empty auth.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.add_services(info) + self.assertJSONEqual(info.as_json( + validate=False), '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image"\n}') + self.assertEqual(info.service, None) + + def test02_just_login(self): + """Test just login.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.login_uri = 'http://example.com/login' + auth.add_services(info) + self.assertEqual(info.service['@id'], "http://example.com/login") + self.assertEqual(info.service['label'], "Login to image server") + self.assertEqual(info.service['profile'], + "http://iiif.io/api/auth/1/login") + + def test03_login_and_logout(self): + """Test login and logout.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.login_uri = 'http://example.com/login' + auth.logout_uri = 'http://example.com/logout' + auth.add_services(info) + self.assertEqual(info.service['@id'], "http://example.com/login") + self.assertEqual(info.service['label'], "Login to image server") + self.assertEqual(info.service['profile'], + "http://iiif.io/api/auth/1/login") + svcs = info.service['service'] + self.assertEqual(svcs['@id'], "http://example.com/logout") + self.assertEqual(svcs['label'], "Logout from image server") + self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/logout") + + def test04_login_and_client_id(self): + """Test login and client id.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.login_uri = 'http://example.com/login' + auth.client_id_uri = 'http://example.com/client_id' + auth.add_services(info) + self.assertEqual(info.service['@id'], "http://example.com/login") + self.assertEqual(info.service['label'], "Login to image server") + self.assertEqual(info.service['profile'], + "http://iiif.io/api/auth/1/login") + svcs = info.service['service'] + self.assertEqual(svcs['@id'], "http://example.com/client_id") + self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/clientId") + + def test05_login_and_access_token(self): + """Test login and access token.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.login_uri = 'http://example.com/login' + auth.access_token_uri = 'http://example.com/token' + auth.add_services(info) + self.assertEqual(info.service['@id'], "http://example.com/login") + self.assertEqual(info.service['label'], "Login to image server") + self.assertEqual(info.service['profile'], + "http://iiif.io/api/auth/1/login") + svcs = info.service['service'] + self.assertEqual(svcs['@id'], "http://example.com/token") + self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/token") + + def test06_full_set(self): + """Test full set of auth services.""" + info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + auth = IIIFAuth() + auth.name = "Whizzo!" + auth.logout_uri = 'http://example.com/logout' + auth.access_token_uri = 'http://example.com/token' + auth.client_id_uri = 'http://example.com/clientId' + auth.login_uri = 'http://example.com/login' + auth.add_services(info) + self.assertEqual(info.service['@id'], "http://example.com/login") + self.assertEqual(info.service['label'], "Login to Whizzo!") + svcs = info.service['service'] + self.assertEqual(svcs[0]['@id'], "http://example.com/logout") + self.assertEqual(svcs[1]['@id'], "http://example.com/clientId") + self.assertEqual(svcs[2]['@id'], "http://example.com/token") diff --git a/tests/test_info_common.py b/tests/test_info_common.py index 2f48fe3..e009ad7 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -18,7 +18,7 @@ def test01_bad_api_versions(self): self.assertRaises(IIIFInfoError, IIIFInfo, api_version='0.9') self.assertRaises(IIIFInfoError, IIIFInfo, api_version='1') self.assertRaises(IIIFInfoError, IIIFInfo, api_version='2.9') - self.assertRaises(IIIFInfoError, IIIFInfo, api_version='3.0') + self.assertRaises(IIIFInfoError, IIIFInfo, api_version='4.0') self.assertRaises(IIIFInfoError, IIIFInfo, api_version='goofy') def test02_id(self): diff --git a/tests/test_request_3_0.py b/tests/test_request_3_0.py new file mode 100644 index 0000000..7120b82 --- /dev/null +++ b/tests/test_request_3_0.py @@ -0,0 +1,396 @@ +"""Test encoding and decoding of IIIF request URLs. + +Follows http://iiif.io/api/image/3.0/ + +This test includes a number of test cases beyond those given +as examples in the table in section 7 of the spec. See +test_request_spec_3_0.py for the set given in the spec. + +Simeon Warner, 2015-05... +""" +import re + +from iiif.error import IIIFError +from iiif.request import IIIFRequest, IIIFRequestError, IIIFRequestBaseURI +from .testlib.request import TestRequests + +# Data for test. Format is +# name : [ {args}, 'canonical_url', 'alternate_form1', ... ] +# +data = { + '00_params': [ + {'identifier': 'id1', 'region': 'full', 'size': 'full', + 'rotation': '0', 'quality': 'default'}, + 'id1/full/full/0/default'], + '02_params': [ + {'identifier': 'id1', 'region': 'full', 'size': '100,100', + 'rotation': '0', 'quality': 'default', 'format': 'jpg'}, + 'id1/full/100,100/0/default.jpg'], + '03_params': [ + {'identifier': 'id1', 'region': 'full', 'size': '100,100', + 'rotation': '0', 'quality': 'gray'}, + 'id1/full/100,100/0/gray'], + '04_params': [ + {'identifier': 'id1', 'region': 'full', 'size': '100,100', + 'rotation': '0', 'quality': 'gray', 'format': 'tif'}, + 'id1/full/100,100/0/gray.tif'], + '05_params': [ + {'identifier': 'bb157hs6068', 'region': 'full', 'size': 'pct:100', + 'rotation': '270', 'quality': 'bitonal', 'format': 'jpg'}, + 'bb157hs6068/full/pct:100/270/bitonal.jpg', + 'bb157hs6068/full/pct%3A100/270/bitonal.jpg'], + '06_params': [ + {'identifier': 'bb157hs6068', 'region': 'full', 'size': '100,', + 'rotation': '123.456', 'quality': 'gray', 'format': 'jpg'}, + 'bb157hs6068/full/100,/123.456/gray.jpg'], + # ARKs from http://tools.ietf.org/html/draft-kunze-ark-00 + # ark:sneezy.dopey.com/12025/654xz321 + # ark:/12025/654xz321 + '21_ARK ': [ + {'identifier': 'ark:sneezy.dopey.com/12025/654xz321', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'ark:sneezy.dopey.com%2F12025%2F654xz321/full/full/0/default', + 'ark%3Asneezy.dopey.com%2F12025%2F654xz321/full/full/0/default'], + '22_ARK ': [ + {'identifier': 'ark:/12025/654xz321', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'ark:%2F12025%2F654xz321/full/full/0/default', + 'ark%3A%2F12025%2F654xz321/full/full/0/default'], + # URNs from http://tools.ietf.org/html/rfc2141 + # urn:foo:a123,456 + '31_URN ': [ + {'identifier': 'urn:foo:a123,456', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'urn:foo:a123,456/full/full/0/default', + 'urn%3Afoo%3Aa123,456/full/full/0/default'], + # URNs from http://en.wikipedia.org/wiki/Uniform_resource_name + # urn:sici:1046-8188(199501)13:1%3C69:FTTHBI%3E2.0.TX;2-4 + # ** note will get double encoding ** + '32_URN ': [ + {'identifier': 'urn:sici:1046-8188(199501)13:1%3C69:FTTHBI%3E2.0.TX;2-4', + 'region': 'full', 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'urn:sici:1046-8188(199501)13:1%253C69:FTTHBI%253E2.0.TX;2-4/full/full/0/default', + 'urn%3Asici%3A1046-8188(199501)13%3A1%253C69%3AFTTHBI%253E2.0.TX;2-4/full/full/0/default'], + # Extreme silliness + '41_odd ': [ + {'identifier': 'http://example.com/?54#a', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'http:%2F%2Fexample.com%2F%3F54%23a/full/full/0/default', + 'http%3A%2F%2Fexample.com%2F?54#a/full/full/0/default'], + # Info requests + '50_info': [ + {'identifier': 'id1', 'info': True, 'format': 'json'}, + 'id1/info.json'], +} + + +class TestAll(TestRequests): + """Tests.""" + + def test01_parse_region(self): + """Parse region.""" + r = IIIFRequest(api_version='3.0') + r.region = None + r.parse_region() + self.assertTrue(r.region_full) + r.region = 'full' + r.parse_region() + self.assertTrue(r.region_full) + self.assertFalse(r.region_square) + r.region = 'square' + r.parse_region() + self.assertFalse(r.region_full) + self.assertTrue(r.region_square) + r.region = '0,1,90,100' + r.parse_region() + self.assertFalse(r.region_full) + self.assertFalse(r.region_pct) + self.assertEqual(r.region_xywh, [0, 1, 90, 100]) + r.region = 'pct:2,3,91,99' + r.parse_region() + self.assertFalse(r.region_full) + self.assertTrue(r.region_pct) + self.assertEqual(r.region_xywh, [2, 3, 91, 99]) + r.region = 'pct:10,10,50,50' + r.parse_region() + self.assertFalse(r.region_full) + self.assertTrue(r.region_pct) + self.assertEqual(r.region_xywh, [10.0, 10.0, 50.0, 50.0]) + r.region = 'pct:0,0,100,100' + r.parse_region() + self.assertFalse(r.region_full) + self.assertTrue(r.region_pct) + self.assertEqual(r.region_xywh, [0.0, 0.0, 100.0, 100.0]) + + def test02_parse_region_bad(self): + """Parse region.""" + r = IIIFRequest(api_version='3.0') + r.region = 'pct:0,0,50,1000' + self.assertRaises(IIIFError, r.parse_region) + r.region = 'pct:-10,0,50,100' + self.assertRaises(IIIFError, r.parse_region) + r.region = 'pct:a,b,c,d' + self.assertRaises(IIIFError, r.parse_region) + r.region = 'a,b,c,d' + self.assertRaises(IIIFError, r.parse_region) + # zero size + r.region = '0,0,0,100' + self.assertRaises(IIIFError, r.parse_region) + r.region = '0,0,100,0' + self.assertRaises(IIIFError, r.parse_region) + # bad name + r.region = '!square' + self.assertRaises(IIIFError, r.parse_region) + r.region = 'square!' + self.assertRaises(IIIFError, r.parse_region) + r.region = '' + self.assertRaises(IIIFError, r.parse_region) + + def test03_parse_size(self): + """Parse size.""" + r = IIIFRequest(api_version='3.0') + r.parse_size('pct:100') + self.assertEqual(r.size_pct, 100.0) + self.assertFalse(r.size_bang) + r.parse_size('1,2') + self.assertFalse(r.size_pct) + self.assertFalse(r.size_bang) + self.assertEqual(r.size_wh, (1, 2)) + r.parse_size('3,') + self.assertFalse(r.size_pct) + self.assertFalse(r.size_bang) + self.assertEqual(r.size_wh, (3, None)) + r.parse_size(',4') + self.assertFalse(r.size_pct) + self.assertFalse(r.size_bang) + self.assertEqual(r.size_wh, (None, 4)) + r.parse_size('!5,6') + self.assertFalse(r.size_pct) + self.assertTrue(r.size_bang) + self.assertEqual(r.size_wh, (5, 6)) + # 'full' + r = IIIFRequest(api_version='3.0') + r.parse_size('full') + self.assertTrue(r.size_full) + self.assertFalse(r.size_max) + self.assertFalse(r.size_pct) + self.assertFalse(r.size_bang) + self.assertEqual(r.size_wh, (None, None)) + # 'max' is new in 3.0 + r = IIIFRequest(api_version='3.0') + r.parse_size('max') + self.assertFalse(r.size_full) + self.assertTrue(r.size_max) + self.assertFalse(r.size_pct) + self.assertFalse(r.size_bang) + self.assertEqual(r.size_wh, (None, None)) + + def test04_parse_size_bad(self): + """Parse size - bad requests.""" + r = IIIFRequest(api_version='3.0') + self.assertRaises(IIIFError, r.parse_size, ',0.0') + self.assertRaises(IIIFError, r.parse_size, '0.0,') + self.assertRaises(IIIFError, r.parse_size, '1.0,1.0') + self.assertRaises(IIIFError, r.parse_size, '1,1,1') + self.assertRaises(IIIFError, r.parse_size, 'bad-size') + # bad pct size + self.assertRaises(IIIFError, r.parse_size, 'pct:a') + self.assertRaises(IIIFError, r.parse_size, 'pct:-1') + # bad bang pixel size + self.assertRaises(IIIFError, r.parse_size, '!1,') + self.assertRaises(IIIFError, r.parse_size, '!,1') + self.assertRaises(IIIFError, r.parse_size, '0,1') + self.assertRaises(IIIFError, r.parse_size, '2,0') + + def test05_parse_rotation(self): + """Parse rotation.""" + r = IIIFRequest(api_version='3.0') + r.parse_rotation('0') + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 0.0) + r.parse_rotation('0.0000') + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 0.0) + r.parse_rotation('0.000001') + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 0.000001) + r.parse_rotation('180') + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 180.0) + r.parse_rotation('360') + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 0.0) + r.parse_rotation('!0') + self.assertEqual(r.rotation_mirror, True) + self.assertEqual(r.rotation_deg, 0.0) + r.parse_rotation('!0.000') + self.assertEqual(r.rotation_mirror, True) + self.assertEqual(r.rotation_deg, 0.0) + r.parse_rotation('!123.45678') + self.assertEqual(r.rotation_mirror, True) + self.assertEqual(r.rotation_deg, 123.45678) + # nothing supplied + r.rotation = None + r.parse_rotation() + self.assertEqual(r.rotation_mirror, False) + self.assertEqual(r.rotation_deg, 0.0) + + def test06_parse_rotation_bad(self): + """Parse rotation - bad requests.""" + r = IIIFRequest(api_version='3.0') + r.rotation = '-1' + self.assertRaises(IIIFError, r.parse_rotation) + r.rotation = '-0.0000001' + self.assertRaises(IIIFError, r.parse_rotation) + r.rotation = '360.0000001' + self.assertRaises(IIIFError, r.parse_rotation) + r.rotation = 'abc' + self.assertRaises(IIIFError, r.parse_rotation) + r.rotation = '1!' + self.assertRaises(IIIFError, r.parse_rotation) + r.rotation = '!!4' + self.assertRaises(IIIFError, r.parse_rotation) + + def test07_parse_quality(self): + """Parse rotation.""" + r = IIIFRequest(api_version='3.0') + r.quality = None + r.parse_quality() + self.assertEqual(r.quality_val, 'default') + r.quality = 'default' + r.parse_quality() + self.assertEqual(r.quality_val, 'default') + r.quality = 'bitonal' + r.parse_quality() + self.assertEqual(r.quality_val, 'bitonal') + r.quality = 'gray' + r.parse_quality() + self.assertEqual(r.quality_val, 'gray') + + def test08_parse_quality_bad(self): + """Parse quality - bad requests.""" + r = IIIFRequest(api_version='3.0') + r.quality = 'does_not_exist' + self.assertRaises(IIIFError, r.parse_quality) + # bad ones + r.quality = '' + self.assertRaises(IIIFError, r.parse_quality) + + def test09_parse_format(self): + """Test parse_format.""" + r = IIIFRequest(api_version='3.0') + r.format = 'jpg' + r.parse_format() + r.format = 'something_else_Z134' + r.parse_format() + # Bad things + r.format = 'no spaces allowed' + self.assertRaises(IIIFRequestError, r.parse_format) + r.format = '~' + self.assertRaises(IIIFRequestError, r.parse_format) + r.format = '' + self.assertRaises(IIIFRequestError, r.parse_format) + + def test10_encode(self): + """Encoding.""" + self.check_encoding(data, '3.0') + + def test11_decode(self): + """Decoding.""" + self.check_decoding(data, '3.0') + + def test12_decode_good(self): + """Decoding examples that should work.""" + r = IIIFRequest(api_version='3.0', baseurl='1.1_netpbm/a/') + r.split_url('1.1_netpbm/a/b/full/full/0/default') + self.assertEqual(r.identifier, 'b') + # id with slashes in it + r = IIIFRequest(api_version='3.0', allow_slashes_in_identifier=True) + r.split_url('a/b/c/full/full/0/default') + self.assertFalse(r.info) + self.assertEqual(r.identifier, 'a/b/c') + r = IIIFRequest(api_version='3.0', allow_slashes_in_identifier=True) + r.split_url('a/b/info.json') + self.assertTrue(r.info) + self.assertEqual(r.identifier, 'a/b') + + def test13_decode_except(self): + """Decoding exceptions.""" + self.assertRaises(IIIFRequestBaseURI, + IIIFRequest(api_version='3.0').split_url, + ("id")) + self.assertRaises(IIIFRequestBaseURI, + IIIFRequest(api_version='3.0').split_url, + ("id%2Ffsdjkh")) + self.assertRaises(IIIFError, + IIIFRequest(api_version='3.0').split_url, + ("id/")) + self.assertRaises(IIIFError, + IIIFRequest(api_version='3.0').split_url, + ("id/bogus")) + self.assertRaises(IIIFError, + IIIFRequest(api_version='3.0').split_url, + ("id1/all/270/!pct%3A75.23.jpg")) + + def test18_url(self): + """Test url() method.""" + r = IIIFRequest(api_version='3.0') + r.size = None + r.size_wh = [11, 22] + self.assertEqual(r.url(identifier='abc1'), + 'abc1/full/11,22/0/default') + r.size_wh = [100, None] + self.assertEqual(r.url(identifier='abc2'), + 'abc2/full/100,/0/default') + r.size_wh = [None, 999] + self.assertEqual(r.url(identifier='abc3'), + 'abc3/full/,999/0/default') + r.size_wh = None + self.assertEqual(r.url(identifier='abc4'), + 'abc4/full/full/0/default') + r = IIIFRequest(api_version='3.0') + r.size_full = True + self.assertEqual(r.url(identifier='abc5'), + 'abc5/full/full/0/default') + r = IIIFRequest(api_version='3.0') + r.size_max = True + self.assertEqual(r.url(identifier='abc5'), + 'abc5/full/max/0/default') + + def test19_split_url(self): + """Test split_url() method. + + Most parts are common to all versions, except handling + of info.json extensions. + """ + # api_version=1.0, format=xyz -> bad + r = IIIFRequest(api_version='1.0') + r.baseurl = 'http://ex.org/a/' + self.assertRaises(IIIFError, r.split_url, + 'http://ex.org/a/b/info.xyz') + # api_version=3.0, format=xml -> bad + r = IIIFRequest(api_version='3.0') + r.baseurl = 'http://ex.org/a/' + self.assertRaises(IIIFError, r.split_url, + 'http://ex.org/a/b/info.xml') + # api_version=3.0, format=xyz -> bad + r = IIIFRequest(api_version='3.0') + r.baseurl = 'http://ex.org/a/' + self.assertRaises(IIIFError, r.split_url, + 'http://ex.org/a/b/info.xyz') + + def test20_bad_response_codes(self): + """Response codes.""" + for (path, code) in [("id/b", 400), + ("id/info.xml", 400), + ("id/b/c", 400), + ("id/b/c/d", 400), + ("id/full/full/0/default.jpg/extra", 400)]: + got_code = None + try: + IIIFRequest(api_version='3.0').split_url(path) + except IIIFError as e: + got_code = e.code + self.assertEqual(got_code, code, + "Bad code %s, expected %d, for path %s" % + (str(got_code), code, path)) diff --git a/tests/test_request_spec_3_0.py b/tests/test_request_spec_3_0.py new file mode 100644 index 0000000..9987bfa --- /dev/null +++ b/tests/test_request_spec_3_0.py @@ -0,0 +1,71 @@ +"""Test encoding and decoding of request URLs of IIIF Image API v3.0. + +This test includes only test cases for the table in section 8, +at . See +test_request_3_0.py for more examples that test other cases and alter +default forms which should still be decoded correctly. + +Simeon Warner - 2015-05... +""" +from iiif.request import IIIFRequest +from .testlib.request import TestRequests + +# Data for test. Format is +# name : [ {args}, 'canonical_url', 'alternate_form1', ... ] +# +data = { + '01_identity': [ + {'identifier': 'id1', 'region': 'full', 'size': 'full', + 'rotation': '0', 'quality': 'default'}, + 'id1/full/full/0/default'], + '02_params': [ + {'identifier': 'id1', 'region': '0,10,100,200', 'size': 'pct:50', + 'rotation': '90', 'quality': 'default', 'format': 'png'}, + 'id1/0,10,100,200/pct:50/90/default.png'], + '03_params': [ + {'identifier': 'id1', 'region': 'pct:10,10,80,80', 'size': '50,', + 'rotation': '22.5', 'quality': 'color', 'format': 'jpg'}, + 'id1/pct:10,10,80,80/50,/22.5/color.jpg'], + '04_params': [ + {'identifier': 'bb157hs6068', 'region': 'full', 'size': 'full', + 'rotation': '270', 'quality': 'gray', 'format': 'jpg'}, + 'bb157hs6068/full/full/270/gray.jpg'], + # ARKs from http://tools.ietf.org/html/draft-kunze-ark-00 + # ark:sneezy.dopey.com/12025/654xz321 + # ark:/12025/654xz321 + '05_ark': [ + {'identifier': 'ark:/12025/654xz321', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'ark:%2F12025%2F654xz321/full/full/0/default'], + # URNs from http://tools.ietf.org/html/rfc2141 + # urn:foo:a123,456 + '06_urn': [ + {'identifier': 'urn:foo:a123,456', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'urn:foo:a123,456/full/full/0/default'], + # URNs from http://en.wikipedia.org/wiki/Uniform_resource_name + # urn:sici:1046-8188(199501)13:1%3C69:FTTHBI%3E2.0.TX;2-4 + # ** note will get double encoding ** + '07_urn': [ + {'identifier': 'urn:sici:1046-8188(199501)13:1%3C69:FTTHBI%3E2.0.TX;2-4', + 'region': 'full', 'size': 'full', 'rotation': '0', 'quality': 'default'}, + + 'urn:sici:1046-8188(199501)13:1%253C69:FTTHBI%253E2.0.TX;2-4/full/full/0/default'], + # Extreme silliness + '08_http': [ + {'identifier': 'http://example.com/?54#a', 'region': 'full', + 'size': 'full', 'rotation': '0', 'quality': 'default'}, + 'http:%2F%2Fexample.com%2F%3F54%23a/full/full/0/default'] +} + + +class TestAll(TestRequests): + """Tests for requests from spec.""" + + def test01_encode(self): + """Encoding.""" + self.check_encoding(data, '3.0') + + def test02_decode(self): + """Decoding.""" + self.check_decoding(data, '3.0') diff --git a/tests/testdata/info_json_3_0/info_bad_context.json b/tests/testdata/info_json_3_0/info_bad_context.json new file mode 100644 index 0000000..12be55b --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_context.json @@ -0,0 +1,31 @@ +{ + "@context" : "http://iiif.io/BAD_CONTEXT.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "sizes" : [ + {"width" : 150, "height" : 100}, + {"width" : 600, "height" : 400}, + {"width" : 3000, "height": 2000} + ], + "tiles": [ + {"width" : 512, "scaleFactors" : [1,2,4,8,16]} + ], + "profile" : [ + "http://iiif.io/api/image/3/level2.json", + { + "formats" : [ "gif", "pdf" ], + "qualities" : [ "color", "gray" ], + "supports" : [ + "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" + ] + } + ], + "service" : { + "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", + "profile": "http://iiif.io/api/annex/service/physdim", + "physicalScale": 0.0025, + "physicalUnits": "in" + } +} diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json new file mode 100644 index 0000000..992d87a --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json @@ -0,0 +1,16 @@ +{ + "@context" : "http://iiif.io/api/image/3/context.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "sizes" : [ + {"width" : 150, "height" : 100}, + {"width" : 600, "height" : 400}, + {"width" : 3000, "height": 2000} + ], + "tiles": [ + {"width" : 512, "scaleFactors" : [1,2,4,8,16]} + ], + "profile" : [ "http://iiif.io/api/image/3/level2.json" ] +} diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json new file mode 100644 index 0000000..8c0c250 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json @@ -0,0 +1,18 @@ +{ + "@context" : "http://iiif.io/api/image/3/context.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 4000, + "height" : 3000, + "profile" : [ + "http://iiif.io/api/image/3/level2.json", + { + "formats" : [ "gif", "pdf" ], + "qualities" : [ "color", "gray" ], + "maxWidth" : 2000, + "supports" : [ + "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" + ] + } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json new file mode 100644 index 0000000..5cdfa91 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json @@ -0,0 +1,58 @@ +{ + "@context" : "http://iiif.io/api/image/3/context.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "sizes" : [ + {"width" : 150, "height" : 100}, + {"width" : 600, "height" : 400}, + {"width" : 3000, "height": 2000} + ], + "tiles": [ + {"width" : 512, "scaleFactors" : [1,2,4]}, + {"width" : 1024, "height" : 2048, "scaleFactors" : [8,16]} + ], + "attribution" : [ + { + "@value" : "Provided by Example Organization", + "@language" : "en" + },{ + "@value" : "Darparwyd gan Enghraifft Sefydliad", + "@language" : "cy" + } + ], + "logo" : { + "@id" : "http://example.org/image-service/logo/full/200,/0/default.png", + "service" : { + "@context" : "http://iiif.io/api/image/3/context.json", + "@id" : "http://example.org/image-service/logo", + "profile" : "http://iiif.io/api/image/3/level2.json" + } + }, + "license" : [ + "http://example.org/rights/license1.html", + "https://creativecommons.org/licenses/by/4.0/" + ], + "profile" : [ + "http://iiif.io/api/image/3/level2.json", + { + "formats" : [ "gif", "pdf" ], + "qualities" : [ "color", "gray" ], + "supports" : [ + "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" + ] + } + ], + "service" : [ + { + "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", + "profile": "http://iiif.io/api/annex/service/physdim", + "physicalScale": 0.0025, + "physicalUnits": "in" + },{ + "@context" : "http://geojson.org/contexts/geojson-base.jsonld", + "@id" : "http://www.example.org/geojson/paris.json" + } + ] +} diff --git a/tests/testdata/info_json_3_0/info_with_extra.json b/tests/testdata/info_json_3_0/info_with_extra.json new file mode 100644 index 0000000..bd701df --- /dev/null +++ b/tests/testdata/info_json_3_0/info_with_extra.json @@ -0,0 +1,34 @@ +{ + "@context" : "http://iiif.io/api/image/3/context.json", + "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "EXTRA1": "SOME EXTRA HERE", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "sizes" : [ + {"width" : 150, "height" : 100}, + {"width" : 600, "height" : 400}, + {"width" : 3000, "height": 2000} + ], + "tiles": [ + {"width" : 512, "scaleFactors" : [1,2,4,8,16]} + ], + "profile" : [ + "http://iiif.io/api/image/3/level2.json", + { + "formats" : [ "gif", "pdf" ], + "qualities" : [ "color", "gray" ], + "EXTRA2" : [ "SOME", "EXTRA", { "HERE": "AND HERE" } ], + "supports" : [ + "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" + ] + } + ], + "service" : { + "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", + "profile": "http://iiif.io/api/annex/service/physdim", + "physicalScale": 0.0025, + "physicalUnits": "in" + }, + "EXTRA3": { "SOME": "EXTRA", "HERE": "TOO" } +} From fef47cc9ade7c8b8219c6bc992984f7ea3363585 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Wed, 21 Feb 2018 15:04:05 +0000 Subject: [PATCH 02/27] Handle @id->id and addition of type --- iiif/info.py | 55 +++++++++++++++---- tests/test_info_3_0.py | 4 +- tests/test_info_3_0_auth_services.py | 2 +- tests/test_info_common.py | 30 ++++++---- .../info_from_spec_section_5_2.json | 2 +- .../info_from_spec_section_5_3.json | 2 +- .../info_from_spec_section_5_6.json | 3 +- .../info_json_3_0/info_with_extra.json | 2 +- tests/testlib/assert_json_equal_mixin.py | 9 ++- 9 files changed, 78 insertions(+), 31 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index 9776521..7948172 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -102,6 +102,8 @@ def _parse_profile(info, json_data): 'compliance_suffix': "", 'protocol': None, 'required_params': ['identifier', 'width', 'height', 'profile'], + 'property_to_json': + {'identifier': '@id'} }, '2.0': { 'params': @@ -122,6 +124,8 @@ def _parse_profile(info, json_data): 'protocol': "http://iiif.io/api/image", 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], + 'property_to_json': + {'identifier': '@id'} }, '2.1': { 'params': @@ -143,10 +147,12 @@ def _parse_profile(info, json_data): 'protocol': "http://iiif.io/api/image", 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], + 'property_to_json': + {'identifier': '@id'} }, '3.0': { 'params': - ['identifier', 'protocol', 'width', 'height', + ['identifier', 'resource_type', 'protocol', 'width', 'height', 'profile', 'sizes', 'tiles', 'service', 'attribution', 'logo', 'license'], # scale_factors isn't in API but used internally @@ -164,6 +170,11 @@ def _parse_profile(info, json_data): 'protocol': "http://iiif.io/api/image", 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], + 'property_to_json': + {'identifier': 'id', + 'resource_type': 'type'}, + 'fixed_values': + {'resource_type': 'ImageService3'} } } @@ -203,8 +214,7 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, if (api_version not in CONF): raise IIIFInfoError( "Unknown IIIF Image API version '%s', versions supported are ('%s')" % - (api_version, sorted( - CONF.keys()))) + (api_version, sorted(CONF.keys()))) self.api_version = api_version self.set_version_info() if (profile is not None): @@ -270,22 +280,44 @@ def id(self, value): self.identifier = value def set_version_info(self, api_version=None): - """Set up normal values for given api_version. + """Set version and load configuration for given api_version. Will use current value of self.api_version if a version number - is not specified in the call. Will raise an IIIFInfoError + is not specified in the call. Will raise IIIFInfoError if an + unknown API version is supplied. + + Sets a number of configuration properties from the content + of CONF[api_version] which then control much of the rest of + the behavior of this object. """ if (api_version is None): api_version = self.api_version if (api_version not in CONF): raise IIIFInfoError("Unknown API version %s" % (api_version)) + # Load configuration for version self.params = CONF[api_version]['params'] self.array_params = CONF[api_version]['array_params'] self.complex_params = CONF[api_version]['complex_params'] for a in ('context', 'compliance_prefix', 'compliance_suffix', - 'protocol', 'required_params'): + 'protocol', 'required_params', 'property_to_json', + 'fixed_values'): if (a in CONF[api_version]): self._setattr(a, CONF[api_version][a]) + # Set any fixed values + if hasattr(self, 'fixed_values'): + for p, v in self.fixed_values.items(): + self._setattr(p, v) + + def json_key(self, property): + """JSON key for given object property name. + + If no mapping is specified then the JSON key is assumed to be + the property name. + """ + try: + return self.property_to_json[property] + except (AttributeError, KeyError): + return property @property def compliance(self): @@ -465,7 +497,7 @@ def as_json(self, validate=True): if (self.api_version == '1.0'): json_dict['identifier'] = self.identifier # local id else: - json_dict['@id'] = self.id # URI + json_dict[self.json_key('identifier')] = self.id # URI params_to_write.discard('profile') if (self.compliance): if (self.api_version < '2.0'): @@ -488,7 +520,7 @@ def as_json(self, validate=True): for param in params_to_write: if (hasattr(self, param) and getattr(self, param) is not None): - json_dict[param] = getattr(self, param) + json_dict[self.json_key(param)] = getattr(self, param) return(json.dumps(json_dict, sort_keys=True, indent=2)) def read(self, fh, api_version=None): @@ -547,10 +579,11 @@ def read(self, fh, api_version=None): else: raise IIIFInfoError("Missing identifier in info.json") else: - if ('@id' in j): - self.id = j['@id'] + id_key = self.json_key('identifier') + if (id_key in j): + self.id = j[id_key] else: - raise IIIFInfoError("Missing @id in info.json") + raise IIIFInfoError("Missing %s in info.json" % (id_key)) # # other params for param in self.params: diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 7b30a82..f89f510 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -13,11 +13,11 @@ def test01_minmal(self): # ?? should this empty case raise and error instead? ir = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') self.assertJSONEqual(ir.as_json(validate=False), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image"\n}') + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') ir.width = 100 ir.height = 200 self.assertJSONEqual(ir.as_json(), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "height": 200, \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", \n "width": 100\n}') + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "height": 200, \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", \n"type": "ImageService3",\n "width": 100\n}') def test04_conf(self): """Tile parameter configuration.""" diff --git a/tests/test_info_3_0_auth_services.py b/tests/test_info_3_0_auth_services.py index 44c5521..77453cd 100644 --- a/tests/test_info_3_0_auth_services.py +++ b/tests/test_info_3_0_auth_services.py @@ -18,7 +18,7 @@ def test01_empty_auth_defined(self): auth = IIIFAuth() auth.add_services(info) self.assertJSONEqual(info.as_json( - validate=False), '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "@id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image"\n}') + validate=False), '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') self.assertEqual(info.service, None) def test02_just_login(self): diff --git a/tests/test_info_common.py b/tests/test_info_common.py index e009ad7..cd45514 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -43,11 +43,9 @@ def test02_id(self): i.id = '/d' self.assertEqual(i.server_and_prefix, '') self.assertEqual(i.identifier, 'd') - - def xx(value): # FIXME - better way to test setter? - i.id = value - self.assertRaises(AttributeError, xx, None) - self.assertRaises(AttributeError, xx, 123) + # id setter + self.assertRaises(AttributeError, setattr, i, 'id', None) + self.assertRaises(AttributeError, setattr, i, 'id', 123) # property i.server_and_prefix = '' i.identifier = '' @@ -59,6 +57,23 @@ def xx(value): # FIXME - better way to test setter? i.identifier = '' self.assertEqual(i.id, 'def/') + def test03_set_version_info(self): + """Test setting of version information.""" + i = IIIFInfo() + self.assertRaises(IIIFInfoError, i.set_version_info, api_version='9.9') + + def test04_json_key(self): + """Test json_key().""" + i = IIIFInfo() + self.assertEqual(i.json_key(None), None) + self.assertEqual(i.json_key(''), '') + self.assertEqual(i.json_key('abc'), 'abc') + i.set_version_info('3.0') + self.assertEqual(i.json_key(None), None) + self.assertEqual(i.json_key(''), '') + self.assertEqual(i.json_key('abc'), 'abc') + self.assertEqual(i.json_key('identifier'), 'id') + def test03_level(self): """Test level handling.""" i = IIIFInfo() @@ -155,11 +170,6 @@ def test07_read(self): i.read(fh) self.assertEqual(i.api_version, '1.1') - def test08_set_version_info(self): - """Test setting of version information.""" - i = IIIFInfo() - self.assertRaises(IIIFInfoError, i.set_version_info, api_version='9.9') - def test09_parse_tiles(self): """Test _parse_tiles function.""" i = IIIFInfo() diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json index 992d87a..98055ac 100644 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json @@ -1,6 +1,6 @@ { "@context" : "http://iiif.io/api/image/3/context.json", - "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", "protocol" : "http://iiif.io/api/image", "width" : 6000, "height" : 4000, diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json index 8c0c250..795be96 100644 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json @@ -1,6 +1,6 @@ { "@context" : "http://iiif.io/api/image/3/context.json", - "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", "protocol" : "http://iiif.io/api/image", "width" : 4000, "height" : 3000, diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json index 5cdfa91..5bf1c6f 100644 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json @@ -1,6 +1,7 @@ { "@context" : "http://iiif.io/api/image/3/context.json", - "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", "protocol" : "http://iiif.io/api/image", "width" : 6000, "height" : 4000, diff --git a/tests/testdata/info_json_3_0/info_with_extra.json b/tests/testdata/info_json_3_0/info_with_extra.json index bd701df..249ea9f 100644 --- a/tests/testdata/info_json_3_0/info_with_extra.json +++ b/tests/testdata/info_json_3_0/info_with_extra.json @@ -1,6 +1,6 @@ { "@context" : "http://iiif.io/api/image/3/context.json", - "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", "EXTRA1": "SOME EXTRA HERE", "protocol" : "http://iiif.io/api/image", "width" : 6000, diff --git a/tests/testlib/assert_json_equal_mixin.py b/tests/testlib/assert_json_equal_mixin.py index 93df4c1..40191ec 100644 --- a/tests/testlib/assert_json_equal_mixin.py +++ b/tests/testlib/assert_json_equal_mixin.py @@ -12,6 +12,9 @@ def assertJSONEqual(self, stra, strb): but before newline, this is not included in python3.x. Strip such spaces before doing the comparison. """ - a = re.sub(r',\s+', ',', stra) - b = re.sub(r',\s+', ',', strb) - self.assertEqual(a, b) + def normalize(json_str): + s = re.sub(r'\s*,\s+', ',\n', json_str) + s = re.sub(r'\s+$', '', s) + return s + self.maxDiff = None + self.assertEqual(normalize(stra), normalize(strb)) From cb3a1ba692431b98c73becbb2306584d73a4590d Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Thu, 22 Feb 2018 13:09:12 +0000 Subject: [PATCH 03/27] Check size constraints --- docs/check_max_algorithm.py | 107 ++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/check_max_algorithm.py diff --git a/docs/check_max_algorithm.py b/docs/check_max_algorithm.py new file mode 100644 index 0000000..46d78d2 --- /dev/null +++ b/docs/check_max_algorithm.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +"""Code to check algorithm for implementing maxArea, maxHeight, maxWidth. + +See: http://iiif.io/api/image/3.0/#technical-properties +""" +import unittest + + +def aspect_ratio_preserving_resize(width, height, scale): + """Acpect ratio preserving scaling of width,height.""" + if (width <= height): + w = int(width * scale) + h = int(height * float(w) / float(width) + 0.5) + else: + h = int(height * scale) + w = int(width * float(h) / float(height) + 0.5) + return(w, h) + + +def max(width, height, maxArea=None, maxHeight=None, maxWidth=None): + """Return /max/ size as w,h given region width,height and constraints.""" + # default without constraints + (w, h) = (width, height) + # use size constraints if present, else full + if maxArea and maxArea < (w * h): + # approximate area limit, rounds down to avoid possibility of + # slightly exceeding maxArea + scale = (float(maxArea) / float(w * h)) ** 0.5 + w = int(w * scale) + h = int(h * scale) + if maxWidth: + if not maxHeight: + maxHeight = maxWidth + if maxWidth < w: + # calculate wrt original width, height rather than + # w, h to avoid compounding rounding issues + w = maxWidth + h = int(float(height * maxWidth) / float(width) + 0.5) + if maxHeight < h: + h = maxHeight + w = int(float(width * maxHeight) / float(height) + 0.5) + # w, h is possibly constrained size + return(w, h) + + +class TestStringMethods(unittest.TestCase): + """Test it...""" + + def max(self, width, height, maxArea=None, maxHeight=None, maxWidth=None): + """Wrapper around max that checks values returned against constraints.""" + (w, h) = max(width, height, maxArea, maxHeight, maxWidth) + if maxWidth: + if not maxHeight: + maxHeight = maxWidth + self.assertLessEqual(w, maxWidth) + if maxHeight: + self.assertLessEqual(h, maxHeight) + if maxArea: + self.assertLessEqual(w * h, maxArea) + return(w, h) + + def test_no_limit(self): + """Test cases where no constraints are specified.""" + self.assertEqual(self.max(1, 1), (1, 1)) + self.assertEqual(self.max(1234, 12345), (1234, 12345)) + + def test_maxArea(self): + """Test cases for maxArea constraint.""" + self.assertEqual(self.max(1, 1, maxArea=10000), (1, 1)) + self.assertEqual(self.max(100, 100, maxArea=10000), (100, 100)) + self.assertEqual(self.max(101, 101, maxArea=10000), (100, 100)) + + def test_maxWidth(self): + """Test cases for maxWidth constraint.""" + self.assertEqual(self.max(1, 1, maxWidth=123), (1, 1)) + self.assertEqual(self.max(123, 123, maxWidth=123), (123, 123)) + self.assertEqual(self.max(124, 124, maxWidth=123), (123, 123)) + self.assertEqual(self.max(6000, 4000, maxWidth=30), (30, 20)) + self.assertEqual(self.max(30, 4000, maxWidth=30), (0, 30)) + + def test_maxWidthHeight(self): + """Test cases for maxWidth and maxHeight constraints.""" + self.assertEqual(self.max(124, 124, maxWidth=123, maxHeight=124), (123, 123)) + self.assertEqual(self.max(124, 124, maxWidth=123, maxHeight=122), (122, 122)) + self.assertEqual(self.max(6000, 4000, maxWidth=1000, maxHeight=20), (30, 20)) + self.assertEqual(self.max(6010, 4000, maxWidth=1000, maxHeight=20), (30, 20)) + self.assertEqual(self.max(6000, 12, maxWidth=1000, maxHeight=20), (1000, 2)) # exact + self.assertEqual(self.max(6000, 14, maxWidth=1000, maxHeight=20), (1000, 2)) + self.assertEqual(self.max(6000, 15, maxWidth=1000, maxHeight=20), (1000, 3)) + self.assertEqual(self.max(6000, 18, maxWidth=1000, maxHeight=20), (1000, 3)) # exact + + def test_maxAreaWidthHeight(self): + """Test cases with combined constraints.""" + self.assertEqual(self.max(110, 110, maxArea=10000, maxWidth=110, maxHeight=110), (100, 100)) + self.assertEqual(self.max(110, 110, maxArea=20000, maxWidth=110, maxHeight=105), (105, 105)) + self.assertEqual(self.max(6000, 1000, maxArea=900000, maxWidth=2000, maxHeight=1000), (2000, 333)) + self.assertEqual(self.max(6000, 30, maxArea=900000, maxWidth=2000, maxHeight=1000), (2000, 10)) + self.assertEqual(self.max(6000, 6000, maxArea=900000, maxWidth=2000, maxHeight=1000), (948, 948)) + self.assertEqual(self.max(4000, 6000, maxArea=900000, maxWidth=2000, maxHeight=1000), (667, 1000)) + self.assertEqual(self.max(60, 6000, maxArea=900000, maxWidth=2000, maxHeight=1000), (10, 1000)) + self.assertEqual(self.max(6000, 6000, maxArea=900, maxWidth=1000, maxHeight=1000), (30, 30)) + self.assertEqual(self.max(6000, 60, maxArea=900, maxWidth=1000, maxHeight=1000), (300, 3)) + self.assertEqual(self.max(6000, 10, maxArea=900, maxWidth=1000, maxHeight=1000), (734, 1)) + + +if __name__ == '__main__': + unittest.main() From 13d42c5440c97a9a60c4a3edf7cce08cf73258db Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Thu, 22 Feb 2018 13:50:52 +0000 Subject: [PATCH 04/27] Start notes --- docs/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fa5eabe --- /dev/null +++ b/docs/README.md @@ -0,0 +1,5 @@ +# IIIF Image API reference implementation + +## Test codes in `docs` + + * `check_max_algorithm.py` - Maximum size calculation implementing `maxArea`, `maxHeight`, `maxWidth` as defined in . Code exceprt used in . \ No newline at end of file From d68c205e3ec61d87abc1fb9eecddf06ff72e3f8f Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Fri, 23 Feb 2018 16:53:02 +0000 Subject: [PATCH 05/27] Improve error tests for v1.x and 2.x --- tests/test_info_1_0.py | 8 ++++---- tests/test_info_1_1.py | 10 +++++----- tests/test_info_2_0.py | 8 ++++---- tests/test_info_2_1.py | 14 +++++++------ ...ad_context.json => info_bad_context1.json} | 0 .../info_json_2_1/info_bad_context2.json | 20 +++++++++++++++++++ 6 files changed, 41 insertions(+), 19 deletions(-) rename tests/testdata/info_json_2_1/{info_bad_context.json => info_bad_context1.json} (100%) create mode 100644 tests/testdata/info_json_2_1/info_bad_context2.json diff --git a/tests/test_info_1_0.py b/tests/test_info_1_0.py index c3364b5..133b33b 100644 --- a/tests/test_info_1_0.py +++ b/tests/test_info_1_0.py @@ -1,7 +1,7 @@ """Test code for iiif/info.py for Image API version 1.0.""" import unittest from .testlib.assert_json_equal_mixin import AssertJSONEqual -from iiif.info import IIIFInfo +from iiif.info import IIIFInfo, IIIFInfoError class TestAll(unittest.TestCase, AssertJSONEqual): @@ -56,9 +56,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo(api_version='1.0') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate i = IIIFInfo(identifier='a') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -82,4 +82,4 @@ def test20_read_unknown_context(self): """Test attempt to read document with bad/unknown @context.""" i = IIIFInfo() fh = open('tests/testdata/info_json_1_0/info_bad_context.json') - self.assertRaises(Exception, i.read, fh) + self.assertRaises(IIIFInfoError, i.read, fh) diff --git a/tests/test_info_1_1.py b/tests/test_info_1_1.py index f983610..8d644c4 100644 --- a/tests/test_info_1_1.py +++ b/tests/test_info_1_1.py @@ -1,7 +1,7 @@ """Test code for iiif/info.py for Image API version 1.1.""" import unittest from .testlib.assert_json_equal_mixin import AssertJSONEqual -from iiif.info import IIIFInfo +from iiif.info import IIIFInfo, IIIFInfoError class TestAll(unittest.TestCase, AssertJSONEqual): @@ -56,9 +56,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate().""" i = IIIFInfo(api_version='1.1') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -88,7 +88,7 @@ def test11_read_example_with_explicit_version(self): i.read(fh) # will get 1.1 from @context self.assertEqual(i.api_version, '1.1') fh = open('tests/testdata/info_json_1_1/info_from_spec.json') - self.assertRaises(Exception, i.read, fh, '0.1') # 0.1 bad + self.assertRaises(IIIFInfoError, i.read, fh, '0.1') # 0.1 bad fh = open('tests/testdata/info_json_1_1/info_from_spec.json') i.read(fh, '1.1') self.assertEqual(i.api_version, '1.1') @@ -97,4 +97,4 @@ def test12_read_example_with_unknown_context(self): """Test read with unknown/bad context.""" i = IIIFInfo() fh = open('tests/testdata/info_json_1_1/info_bad_context.json') - self.assertRaises(Exception, i.read, fh) + self.assertRaises(IIIFInfoError, i.read, fh) diff --git a/tests/test_info_2_0.py b/tests/test_info_2_0.py index 5be4977..588e188 100644 --- a/tests/test_info_2_0.py +++ b/tests/test_info_2_0.py @@ -2,7 +2,7 @@ import unittest from .testlib.assert_json_equal_mixin import AssertJSONEqual import json -from iiif.info import IIIFInfo +from iiif.info import IIIFInfo, IIIFInfoError class TestAll(unittest.TestCase, AssertJSONEqual): @@ -48,9 +48,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo() - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -103,7 +103,7 @@ def test12_read_unknown_context(self): """Test read with bad/unknown @context.""" i = IIIFInfo() fh = open('tests/testdata/info_json_2_0/info_bad_context.json') - self.assertRaises(Exception, i.read, fh) + self.assertRaises(IIIFInfoError, i.read, fh) def test20_write_example_in_spec(self): """Test creation of example from spec.""" diff --git a/tests/test_info_2_1.py b/tests/test_info_2_1.py index 41a89ab..4336f7c 100644 --- a/tests/test_info_2_1.py +++ b/tests/test_info_2_1.py @@ -2,7 +2,7 @@ import unittest from .testlib.assert_json_equal_mixin import AssertJSONEqual import json -from iiif.info import IIIFInfo +from iiif.info import IIIFInfo, IIIFInfoError class TestAll(unittest.TestCase, AssertJSONEqual): @@ -52,9 +52,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo(api_version='2.1') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFError, i.validate i = IIIFInfo(identifier='a') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFError, i.validate i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -145,9 +145,11 @@ def test11_read_example_with_extra(self): def test12_read_unknown_context(self): """Test bad/unknown context.""" - i = IIIFInfo(api_version='2.1') - fh = open('tests/testdata/info_json_2_1/info_bad_context.json') - self.assertRaises(Exception, i.read, fh) + for ctx_file in ('tests/testdata/info_json_2_1/info_bad_context1.json', + 'tests/testdata/info_json_2_1/info_bad_context2.json'): + i = IIIFInfo(api_version='2.1') + fh = open(ctx_file) + self.assertRaises(IIIFError, i.read, fh) def test20_write_example_in_spec(self): """Create example info.json in spec.""" diff --git a/tests/testdata/info_json_2_1/info_bad_context.json b/tests/testdata/info_json_2_1/info_bad_context1.json similarity index 100% rename from tests/testdata/info_json_2_1/info_bad_context.json rename to tests/testdata/info_json_2_1/info_bad_context1.json diff --git a/tests/testdata/info_json_2_1/info_bad_context2.json b/tests/testdata/info_json_2_1/info_bad_context2.json new file mode 100644 index 0000000..ebfb226 --- /dev/null +++ b/tests/testdata/info_json_2_1/info_bad_context2.json @@ -0,0 +1,20 @@ +{ + "@context" : [ + "http://example.org/array/context/illegal/in/2.1.json", + "http://iiif.io/api/image/2/context.json" + } + "@id" : "http://example.org/imageabcd1234", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : [ + "http://iiif.io/api/image/2/level2.json", + { + "formats" : [ "gif", "pdf" ], + "qualities" : [ "color", "gray" ], + "supports" : [ + "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" + ] + } + ] +} From a41b650d81c10fb05e49118984fabf3bb14d2e58 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Fri, 23 Feb 2018 17:10:00 +0000 Subject: [PATCH 06/27] Accept array or string @context for 3.0 --- iiif/info.py | 44 +++++++++++++------ tests/test_info_1_0.py | 4 +- tests/test_info_2_1.py | 6 +-- tests/test_info_3_0.py | 42 ++++++++++++------ .../info_json_2_1/info_bad_context2.json | 2 +- .../info_json_3_0/info_bad_context.json | 31 ------------- .../info_json_3_0/info_bad_context1.json | 11 +++++ .../info_json_3_0/info_bad_context2.json | 14 ++++++ .../info_json_3_0/info_bad_context3.json | 11 +++++ .../info_json_3_0/info_good_contex2.json | 8 ++++ .../info_json_3_0/info_good_contex3.json | 11 +++++ .../info_json_3_0/info_good_context1.json | 8 ++++ .../info_json_3_0/info_good_context2.json | 8 ++++ .../info_json_3_0/info_good_context3.json | 11 +++++ 14 files changed, 148 insertions(+), 63 deletions(-) delete mode 100644 tests/testdata/info_json_3_0/info_bad_context.json create mode 100644 tests/testdata/info_json_3_0/info_bad_context1.json create mode 100644 tests/testdata/info_json_3_0/info_bad_context2.json create mode 100644 tests/testdata/info_json_3_0/info_bad_context3.json create mode 100644 tests/testdata/info_json_3_0/info_good_contex2.json create mode 100644 tests/testdata/info_json_3_0/info_good_contex3.json create mode 100644 tests/testdata/info_json_3_0/info_good_context1.json create mode 100644 tests/testdata/info_json_3_0/info_good_context2.json create mode 100644 tests/testdata/info_json_3_0/info_good_context3.json diff --git a/iiif/info.py b/iiif/info.py index 7948172..c9cdb5f 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -13,18 +13,25 @@ import re +def _parse_string_or_array(info, json_data): + # Parse either JSON single string or array into array. + if (not isinstance(json_data, list)): + json_data = [json_data] + return json_data + + def _parse_int_array(info, json_data): - # Force simple array of interger values + # Force simple array of interger values. return [int(x) for x in json_data] def _parse_noop(info, json_data): - # Format is already what we want + # Format is already what we want. return json_data def _parse_tile(info, json_data): - # Parse data for a single tile specification + # Parse data for a single tile specification. tile = {} tile['width'] = int(json_data['width']) if ('height' in json_data): @@ -34,7 +41,7 @@ def _parse_tile(info, json_data): def _parse_tiles(info, json_data): - # Parse tiles array of tile specifications + # Parse tiles array of tile specifications. # # Expect common case in 2.0 to map to 1.1 idea of tile_width, # tile_height and scale_factors. This is the case when len()==1. @@ -180,7 +187,13 @@ def _parse_profile(info, json_data): class IIIFInfoError(Exception): - """IIIFInfoErrors from IIIFInfo.""" + """IIIFInfoError from IIIFInfo.""" + + pass + + +class IIIFInfoContextError(IIIFInfoError): + """IIIFInfoContextError for @context issues from IIIFInfo.""" pass @@ -533,42 +546,47 @@ def read(self, fh, api_version=None): If api_version is set then the parsing will assume this API version, else the version will be determined from the incoming data. NOTE that the value of self.api_version is NOT used in this routine. + If an api_version is specified and there is a @context specified then - an IIIFInfoError will be raised unless these match. If no known - @context is present and no api_version set then an IIIFInfoError + an IIIFInfoContextError will be raised unless these match. If no known + @context is present and no api_version set then an IIIFInfoContextError will be raised. """ j = json.load(fh) # # @context and API version self.context = None + self.contexts = None if (api_version == '1.0'): - # v1.0 did not have a @context so we simply take the version - # passed in + # v1.0 did not have a @context so we simply take the + # version passed in self.api_version = api_version elif ('@context' in j): # determine API version from context - self.context = j['@context'] + self.contexts = _parse_string_or_array(self, j['@context']) + self.context = self.contexts[-1] api_version_read = None for v in CONF: if (v > '1.0' and self.context == CONF[v]['context']): api_version_read = v break if (api_version_read is None): - raise IIIFInfoError( + raise IIIFInfoContextError( "Unknown @context, cannot determine API version (%s)" % (self.context)) else: if (api_version is not None and api_version != api_version_read): - raise IIIFInfoError( + raise IIIFInfoContextError( "Expected API version '%s' but got @context for API version '%s'" % (api_version, api_version_read)) else: self.api_version = api_version_read + if self.api_version < '3.0' and len(self.contexts) > 1: + raise IIIFInfoContextError("Multiple @contexts not allowed in versions prior to v3.0") else: # no @context and not 1.0 if (api_version is None): - raise IIIFInfoError("No @context (and no default given)") + raise IIIFInfoContextError("No @context (and no default given)") self.api_version = api_version self.set_version_info() # diff --git a/tests/test_info_1_0.py b/tests/test_info_1_0.py index 133b33b..d0431a2 100644 --- a/tests/test_info_1_0.py +++ b/tests/test_info_1_0.py @@ -56,9 +56,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo(api_version='1.0') - self.assertRaises(IIIFInfoError, i.validate + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a') - self.assertRaises(IIIFInfoError, i.validate + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) diff --git a/tests/test_info_2_1.py b/tests/test_info_2_1.py index 4336f7c..3920990 100644 --- a/tests/test_info_2_1.py +++ b/tests/test_info_2_1.py @@ -52,9 +52,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo(api_version='2.1') - self.assertRaises(IIIFError, i.validate + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a') - self.assertRaises(IIIFError, i.validate + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -149,7 +149,7 @@ def test12_read_unknown_context(self): 'tests/testdata/info_json_2_1/info_bad_context2.json'): i = IIIFInfo(api_version='2.1') fh = open(ctx_file) - self.assertRaises(IIIFError, i.read, fh) + self.assertRaises(IIIFInfoError, i.read, fh) def test20_write_example_in_spec(self): """Create example info.json in spec.""" diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index f89f510..253b214 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -2,12 +2,17 @@ import unittest from .testlib.assert_json_equal_mixin import AssertJSONEqual import json -from iiif.info import IIIFInfo +import os.path +from iiif.info import IIIFInfo, IIIFInfoError, IIIFInfoContextError class TestAll(unittest.TestCase, AssertJSONEqual): """Tests.""" + def open_testdata(self, filename): + """Open test data file.""" + return open(os.path.join('tests/testdata/info_json_3_0', filename)) + def test01_minmal(self): """Trivial JSON test.""" # ?? should this empty case raise and error instead? @@ -52,9 +57,9 @@ def test05_level_and_profile(self): def test06_validate(self): """Test validate method.""" i = IIIFInfo(api_version='3.0') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a') - self.assertRaises(Exception, i.validate, ()) + self.assertRaises(IIIFInfoError, i.validate) i = IIIFInfo(identifier='a', width=1, height=2) self.assertTrue(i.validate()) @@ -62,7 +67,7 @@ def test10_read_examples_from_spec(self): """Test reading of examples from spec.""" # Section 5.2, full example i = IIIFInfo(api_version='3.0') - fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_2.json') + fh = self.open_testdata('info_from_spec_section_5_2.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") @@ -88,7 +93,7 @@ def test10_read_examples_from_spec(self): # Section 5.3, full example i = IIIFInfo(api_version='3.0') - fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_3.json') + fh = self.open_testdata('info_from_spec_section_5_3.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") @@ -111,7 +116,7 @@ def test10_read_examples_from_spec(self): # Section 5.6, full example i = IIIFInfo(api_version='3.0') - fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json') + fh = self.open_testdata('info_from_spec_section_5_6.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") @@ -127,7 +132,7 @@ def test10_read_examples_from_spec(self): def test11_read_example_with_extra(self): """Test read of exampe with extra info.""" i = IIIFInfo(api_version='3.0') - fh = open('tests/testdata/info_json_3_0/info_with_extra.json') + fh = self.open_testdata('info_with_extra.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") @@ -143,11 +148,23 @@ def test11_read_example_with_extra(self): self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16]) self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json") - def test12_read_unknown_context(self): + def test12_read_bad_context(self): """Test bad/unknown context.""" - i = IIIFInfo(api_version='3.0') - fh = open('tests/testdata/info_json_3_0/info_bad_context.json') - self.assertRaises(Exception, i.read, fh) + for ctx_file in ['info_bad_context1.json', + 'info_bad_context2.json', + 'info_bad_context3.json']: + i = IIIFInfo(api_version='3.0') + fh = self.open_testdata(ctx_file) + self.assertRaises(IIIFInfoContextError, i.read, fh) + + def test13_read_good_context(self): + """Test good context.""" + for ctx_file in ['info_good_context1.json', + 'info_good_context2.json', + 'info_good_context3.json']: + i = IIIFInfo(api_version='3.0') + i.read(self.open_testdata(ctx_file)) + self.assertEqual(i.api_version, '3.0') def test20_write_example_in_spec(self): """Create example info.json in spec.""" @@ -191,8 +208,7 @@ def test20_write_example_in_spec(self): "@id": "http://www.example.org/geojson/paris.json"}] ) reparsed_json = json.loads(i.as_json()) - example_json = json.load( - open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json')) + example_json = json.load(self.open_testdata('info_from_spec_section_5_6.json')) self.maxDiff = 4000 self.assertEqual(reparsed_json, example_json) diff --git a/tests/testdata/info_json_2_1/info_bad_context2.json b/tests/testdata/info_json_2_1/info_bad_context2.json index ebfb226..5d62580 100644 --- a/tests/testdata/info_json_2_1/info_bad_context2.json +++ b/tests/testdata/info_json_2_1/info_bad_context2.json @@ -2,7 +2,7 @@ "@context" : [ "http://example.org/array/context/illegal/in/2.1.json", "http://iiif.io/api/image/2/context.json" - } + ], "@id" : "http://example.org/imageabcd1234", "protocol" : "http://iiif.io/api/image", "width" : 6000, diff --git a/tests/testdata/info_json_3_0/info_bad_context.json b/tests/testdata/info_json_3_0/info_bad_context.json deleted file mode 100644 index 12be55b..0000000 --- a/tests/testdata/info_json_3_0/info_bad_context.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context" : "http://iiif.io/BAD_CONTEXT.json", - "@id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", - "protocol" : "http://iiif.io/api/image", - "width" : 6000, - "height" : 4000, - "sizes" : [ - {"width" : 150, "height" : 100}, - {"width" : 600, "height" : 400}, - {"width" : 3000, "height": 2000} - ], - "tiles": [ - {"width" : 512, "scaleFactors" : [1,2,4,8,16]} - ], - "profile" : [ - "http://iiif.io/api/image/3/level2.json", - { - "formats" : [ "gif", "pdf" ], - "qualities" : [ "color", "gray" ], - "supports" : [ - "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" - ] - } - ], - "service" : { - "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", - "profile": "http://iiif.io/api/annex/service/physdim", - "physicalScale": 0.0025, - "physicalUnits": "in" - } -} diff --git a/tests/testdata/info_json_3_0/info_bad_context1.json b/tests/testdata/info_json_3_0/info_bad_context1.json new file mode 100644 index 0000000..a34861b --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_context1.json @@ -0,0 +1,11 @@ +{ + "@context" : "http://iiif.io/BAD_CONTEXT.json", + "id" : "http://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2", + "extraFormats": [ "gif", "pdf" ], + "extraQualities": [ "color", "gray" ], + "extraFeatures": [ "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" ] +} diff --git a/tests/testdata/info_json_3_0/info_bad_context2.json b/tests/testdata/info_json_3_0/info_bad_context2.json new file mode 100644 index 0000000..4379fa4 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_context2.json @@ -0,0 +1,14 @@ +{ + "@context": [ + "http://example.org/some/context.json", + "http://iiif.io/BAD_CONTEXT.json" + ], + "id" : "http://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2", + "extraFormats": [ "gif", "pdf" ], + "extraQualities": [ "color", "gray" ], + "extraFeatures": [ "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" ] +} diff --git a/tests/testdata/info_json_3_0/info_bad_context3.json b/tests/testdata/info_json_3_0/info_bad_context3.json new file mode 100644 index 0000000..cbb0d22 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_context3.json @@ -0,0 +1,11 @@ +{ + "@context" : [ + "http://iiif.io/api/image/3/context.json", + "http://example.org/some/context/that/should/not/be/last.json" + ], + "id" : "http://example.org/imgABC", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2" +} diff --git a/tests/testdata/info_json_3_0/info_good_contex2.json b/tests/testdata/info_json_3_0/info_good_contex2.json new file mode 100644 index 0000000..a5a646e --- /dev/null +++ b/tests/testdata/info_json_3_0/info_good_contex2.json @@ -0,0 +1,8 @@ +{ + "@context": [ "http://iiif.io/api/image/3/context.json" ], + "id" : "http://example.org/imgABC", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2" +} diff --git a/tests/testdata/info_json_3_0/info_good_contex3.json b/tests/testdata/info_json_3_0/info_good_contex3.json new file mode 100644 index 0000000..a2e7d7f --- /dev/null +++ b/tests/testdata/info_json_3_0/info_good_contex3.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "http://example.org/some/context.json", + "http://iiif.io/api/image/3/context.json" + ], + "id" : "http://example.org/imgABC", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2" +} diff --git a/tests/testdata/info_json_3_0/info_good_context1.json b/tests/testdata/info_json_3_0/info_good_context1.json new file mode 100644 index 0000000..b4ef6ce --- /dev/null +++ b/tests/testdata/info_json_3_0/info_good_context1.json @@ -0,0 +1,8 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "http://example.org/imgABC", + "protocol": "http://iiif.io/api/image", + "width": 6000, + "height": 4000, + "profile": "level2" +} diff --git a/tests/testdata/info_json_3_0/info_good_context2.json b/tests/testdata/info_json_3_0/info_good_context2.json new file mode 100644 index 0000000..a5a646e --- /dev/null +++ b/tests/testdata/info_json_3_0/info_good_context2.json @@ -0,0 +1,8 @@ +{ + "@context": [ "http://iiif.io/api/image/3/context.json" ], + "id" : "http://example.org/imgABC", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2" +} diff --git a/tests/testdata/info_json_3_0/info_good_context3.json b/tests/testdata/info_json_3_0/info_good_context3.json new file mode 100644 index 0000000..a2e7d7f --- /dev/null +++ b/tests/testdata/info_json_3_0/info_good_context3.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "http://example.org/some/context.json", + "http://iiif.io/api/image/3/context.json" + ], + "id" : "http://example.org/imgABC", + "protocol" : "http://iiif.io/api/image", + "width" : 6000, + "height" : 4000, + "profile" : "level2" +} From 74569283967242316bb1edcfb95c8967ddbcfa0b Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Fri, 23 Feb 2018 20:41:17 +0000 Subject: [PATCH 07/27] Do not parse info into _parse_* functions --- iiif/info.py | 24 ++++++++++++------------ tests/test_info_common.py | 9 +++------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index c9cdb5f..e8e253e 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -13,34 +13,34 @@ import re -def _parse_string_or_array(info, json_data): +def _parse_string_or_array(json_data): # Parse either JSON single string or array into array. if (not isinstance(json_data, list)): json_data = [json_data] return json_data -def _parse_int_array(info, json_data): +def _parse_int_array(json_data): # Force simple array of interger values. return [int(x) for x in json_data] -def _parse_noop(info, json_data): +def _parse_noop(json_data): # Format is already what we want. return json_data -def _parse_tile(info, json_data): +def _parse_tile(json_data): # Parse data for a single tile specification. tile = {} tile['width'] = int(json_data['width']) if ('height' in json_data): tile['height'] = int(json_data['height']) - tile['scaleFactors'] = _parse_int_array(info, json_data['scaleFactors']) + tile['scaleFactors'] = _parse_int_array(json_data['scaleFactors']) return tile -def _parse_tiles(info, json_data): +def _parse_tiles(json_data): # Parse tiles array of tile specifications. # # Expect common case in 2.0 to map to 1.1 idea of tile_width, @@ -49,15 +49,15 @@ def _parse_tiles(info, json_data): if (len(json_data) == 0): raise IIIFInfoError("Empty tiles array property not allowed.") for tile_spec in json_data: - tiles.append(_parse_tile(info, tile_spec)) + tiles.append(_parse_tile(tile_spec)) return tiles -def _parse_service(info, json_data): +def _parse_service(json_data): return json_data -def _parse_profile(info, json_data): +def _parse_profile(json_data): # 2.1 spec: "A list of profiles, indicated by either a URI or an # object describing the features supported. The first entry # in the list must be a compliance level URI." @@ -563,7 +563,7 @@ def read(self, fh, api_version=None): self.api_version = api_version elif ('@context' in j): # determine API version from context - self.contexts = _parse_string_or_array(self, j['@context']) + self.contexts = _parse_string_or_array(j['@context']) self.context = self.contexts[-1] api_version_read = None for v in CONF: @@ -611,8 +611,8 @@ def read(self, fh, api_version=None): if (param in self.complex_params): # use function ref in complex_params to parse, optional # dst to map to a different property name - self._setattr(param, self.complex_params[ - param](self, j[param])) + self._setattr(param, + self.complex_params[param](j[param])) else: self._setattr(param, j[param]) return True diff --git a/tests/test_info_common.py b/tests/test_info_common.py index cd45514..9cec4c4 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -172,16 +172,13 @@ def test07_read(self): def test09_parse_tiles(self): """Test _parse_tiles function.""" - i = IIIFInfo() jd = [{'width': 512, 'scaleFactors': [1, 2, 4]}] - self.assertEqual(_parse_tiles(i, jd), jd) - i = IIIFInfo() + self.assertEqual(_parse_tiles(jd), jd) jd = [{'width': 256, 'height': 200, 'scaleFactors': [1]}] - self.assertEqual(_parse_tiles(i, jd), jd) + self.assertEqual(_parse_tiles(jd), jd) # error case - empty tiles - i = IIIFInfo() jd = [] - self.assertRaises(IIIFInfoError, _parse_tiles, i, jd) + self.assertRaises(IIIFInfoError, _parse_tiles, jd) def test20_scale_factors(self): """Test getter/setter for scale_factors property.""" From e40d6ef785b7fd4afc6bda27a8252a5228f6b234 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Fri, 23 Feb 2018 23:47:56 +0000 Subject: [PATCH 08/27] Tidy @context parsing --- iiif/info.py | 121 ++++++++++++++++++++++---------------- tests/test_info_common.py | 5 +- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index e8e253e..c3573cb 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -77,28 +77,38 @@ def _parse_profile(json_data): CONF = { '1.0': { - 'params': - ['identifier', 'protocol', 'width', 'height', 'scale_factors', - 'tile_width', 'tile_height', 'formats', 'qualities', 'profile'], - 'array_params': set( - ['scale_factors', 'formats', 'qualities']), + 'params': [ + 'at_contexts', 'identifier', 'protocol', 'width', 'height', + 'scale_factors', 'tile_width', 'tile_height', 'formats', + 'qualities', 'profile' + ], + 'array_params': set([ + 'scale_factors', 'formats', 'qualities' + ]), 'complex_params': { + 'at_contexts': _parse_string_or_array, 'scale_factors': _parse_int_array, 'formats': _parse_noop, # array of str - 'qualities': _parse_noop}, # array of str + 'qualities': _parse_noop # array of str + }, 'compliance_prefix': "http://library.stanford.edu/iiif/image-api/compliance.html#level", 'compliance_suffix': "", 'protocol': None, 'required_params': ['identifier', 'width', 'height', 'profile'], + 'property_to_json': { + 'at_contexts': '@context' + } }, '1.1': { 'params': - ['identifier', 'protocol', 'width', 'height', 'scale_factors', - 'tile_width', 'tile_height', 'formats', 'qualities', 'profile'], + ['at_contexts', 'identifier', 'protocol', 'width', 'height', + 'scale_factors', 'tile_width', 'tile_height', 'formats', + 'qualities', 'profile'], 'array_params': set( ['scale_factors', 'formats', 'qualities']), 'complex_params': { + 'at_contexts': _parse_string_or_array, 'scale_factors': _parse_int_array, 'formats': _parse_noop, # array of str 'qualities': _parse_noop}, # array of str @@ -110,17 +120,19 @@ def _parse_profile(json_data): 'protocol': None, 'required_params': ['identifier', 'width', 'height', 'profile'], 'property_to_json': - {'identifier': '@id'} + {'at_contexts': '@context', + 'identifier': '@id'} }, '2.0': { 'params': - ['identifier', 'protocol', 'width', 'height', 'profile', - 'sizes', 'tiles', 'service'], + ['at_contexts', 'identifier', 'protocol', 'width', 'height', + 'profile', 'sizes', 'tiles', 'service'], # scale_factors isn't in API but used internally 'array_params': set( ['sizes', 'tiles', 'service', 'scale_factors', 'formats', 'qualities', 'supports']), 'complex_params': { + 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, @@ -132,11 +144,12 @@ def _parse_profile(json_data): 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], 'property_to_json': - {'identifier': '@id'} + {'at_contexts': '@context', + 'identifier': '@id'} }, '2.1': { 'params': - ['identifier', 'protocol', 'width', 'height', + ['at_contexts', 'identifier', 'protocol', 'width', 'height', 'profile', 'sizes', 'tiles', 'service', 'attribution', 'logo', 'license'], # scale_factors isn't in API but used internally @@ -144,6 +157,7 @@ def _parse_profile(json_data): ['sizes', 'tiles', 'service', 'scale_factors', 'formats', 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']), 'complex_params': { + 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, @@ -155,18 +169,20 @@ def _parse_profile(json_data): 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], 'property_to_json': - {'identifier': '@id'} + {'at_contexts': '@context', + 'identifier': '@id'} }, '3.0': { 'params': - ['identifier', 'resource_type', 'protocol', 'width', 'height', - 'profile', 'sizes', 'tiles', 'service', + ['at_contexts', 'identifier', 'resource_type', 'protocol', + 'width', 'height', 'profile', 'sizes', 'tiles', 'service', 'attribution', 'logo', 'license'], # scale_factors isn't in API but used internally 'array_params': set( ['sizes', 'tiles', 'service', 'scale_factors', 'formats', 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']), 'complex_params': { + 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, @@ -178,7 +194,8 @@ def _parse_profile(json_data): 'required_params': ['identifier', 'protocol', 'width', 'height', 'profile'], 'property_to_json': - {'identifier': 'id', + {'at_contexts': '@context', + 'identifier': 'id', 'resource_type': 'type'}, 'fixed_values': {'resource_type': 'ImageService3'} @@ -204,7 +221,7 @@ class IIIFInfo(object): def __init__(self, api_version='2.1', profile=None, level=1, conf=None, server_and_prefix='', identifier=None, width=None, height=None, tiles=None, - sizes=None, service=None, id=None, + sizes=None, service=None, id=None, at_contexts=None, # legacy params from 1.1 scale_factors=None, tile_width=None, tile_height=None, # 1.1 and 2.0 @@ -260,6 +277,7 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, self.attribution = attribution self.logo = logo self.license = license + self.at_contexts = at_contexts # defaults from conf dict if provided if (conf): for option in conf: @@ -536,6 +554,19 @@ def as_json(self, validate=True): json_dict[self.json_key(param)] = getattr(self, param) return(json.dumps(json_dict, sort_keys=True, indent=2)) + def read_property(self, j, param): + """Read one property param from JSON j.""" + key = self.json_key(param) + print("param=%s key=%s" % (param, key)) + if (key in j): + value = j[key] + print("got %s %s" % (param, str(value))) + if (param in self.complex_params): + # use function ref in self.complex_params to parse + value = self.complex_params[param](value) + print("set %s %s" % (param, str(value))) + self._setattr(param, value) + def read(self, fh, api_version=None): """Read info.json from file like object. @@ -552,37 +583,34 @@ def read(self, fh, api_version=None): @context is present and no api_version set then an IIIFInfoContextError will be raised. """ + # load and parse JSON j = json.load(fh) - # - # @context and API version + # must work out API version in order to know how to parse JSON + # extract image API specific @context and API version self.context = None - self.contexts = None + self.read_property(j, 'at_contexts') if (api_version == '1.0'): - # v1.0 did not have a @context so we simply take the - # version passed in + # v1.0 did not have a @context so take the version passed in self.api_version = api_version - elif ('@context' in j): - # determine API version from context - self.contexts = _parse_string_or_array(j['@context']) - self.context = self.contexts[-1] + elif self.at_contexts is not None: + # determine API version from last context + self.context = self.at_contexts[-1] api_version_read = None for v in CONF: if (v > '1.0' and self.context == CONF[v]['context']): api_version_read = v break - if (api_version_read is None): + if api_version_read is None: raise IIIFInfoContextError( "Unknown @context, cannot determine API version (%s)" % (self.context)) - else: - if (api_version is not None and - api_version != api_version_read): - raise IIIFInfoContextError( - "Expected API version '%s' but got @context for API version '%s'" % - (api_version, api_version_read)) - else: - self.api_version = api_version_read - if self.api_version < '3.0' and len(self.contexts) > 1: + elif api_version is None: + self.api_version = api_version_read + elif api_version != api_version_read: + raise IIIFInfoContextError( + "Expected API version '%s' but got @context for API version '%s'" % + (api_version, api_version_read)) + if self.api_version < '3.0' and len(self.at_contexts) > 1: raise IIIFInfoContextError("Multiple @contexts not allowed in versions prior to v3.0") else: # no @context and not 1.0 if (api_version is None): @@ -590,7 +618,12 @@ def read(self, fh, api_version=None): self.api_version = api_version self.set_version_info() # - # @id or identifier + # parse remaining JSON top-level keys + for param in self.params: + if param != 'at_contexts': + self.read_property(j, param) + # + # @id or identifier (1.0 only) if (self.api_version == '1.0'): if ('identifier' in j): self.id = j['identifier'] @@ -603,16 +636,4 @@ def read(self, fh, api_version=None): else: raise IIIFInfoError("Missing %s in info.json" % (id_key)) # - # other params - for param in self.params: - if (param == 'identifier'): - continue # dealt with above - if (param in j): - if (param in self.complex_params): - # use function ref in complex_params to parse, optional - # dst to map to a different property name - self._setattr(param, - self.complex_params[param](j[param])) - else: - self._setattr(param, j[param]) return True diff --git a/tests/test_info_common.py b/tests/test_info_common.py index 9cec4c4..8a427d2 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -163,10 +163,9 @@ def test07_read(self): # bad @context fh = io.StringIO('{ "@context": "oops" }') self.assertRaises(IIIFInfoError, i.read, fh) - # + # minimal 1.1 -- @context and @id fh = io.StringIO( - '{ "@context": "http://library.stanford.edu/' - 'iiif/image-api/1.1/context.json", "@id": "a" }') + '{ "@context": "http://library.stanford.edu/iiif/image-api/1.1/context.json", "@id": "a" }') i.read(fh) self.assertEqual(i.api_version, '1.1') From 3ef6ca266a0c61c3d1de353633e4b3792255229e Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sat, 24 Feb 2018 00:58:12 +0000 Subject: [PATCH 09/27] Improve @context handling --- iiif/info.py | 244 +++++++++++++++++++++----------------- tests/test_info_3_0.py | 2 +- tests/test_info_common.py | 2 +- 3 files changed, 135 insertions(+), 113 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index c3573cb..7cc41ee 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -78,7 +78,7 @@ def _parse_profile(json_data): CONF = { '1.0': { 'params': [ - 'at_contexts', 'identifier', 'protocol', 'width', 'height', + 'id', 'protocol', 'width', 'height', 'scale_factors', 'tile_width', 'tile_height', 'formats', 'qualities', 'profile' ], @@ -86,7 +86,6 @@ def _parse_profile(json_data): 'scale_factors', 'formats', 'qualities' ]), 'complex_params': { - 'at_contexts': _parse_string_or_array, 'scale_factors': _parse_int_array, 'formats': _parse_noop, # array of str 'qualities': _parse_noop # array of str @@ -95,110 +94,123 @@ def _parse_profile(json_data): "http://library.stanford.edu/iiif/image-api/compliance.html#level", 'compliance_suffix': "", 'protocol': None, - 'required_params': ['identifier', 'width', 'height', 'profile'], + 'required_params': ['id', 'width', 'height', 'profile'], 'property_to_json': { - 'at_contexts': '@context' + 'id': 'identifier' } }, '1.1': { - 'params': - ['at_contexts', 'identifier', 'protocol', 'width', 'height', - 'scale_factors', 'tile_width', 'tile_height', 'formats', - 'qualities', 'profile'], - 'array_params': set( - ['scale_factors', 'formats', 'qualities']), + 'params': [ + 'id', 'protocol', 'width', 'height', + 'scale_factors', 'tile_width', 'tile_height', 'formats', + 'qualities', 'profile' + ], + 'array_params': set([ + 'scale_factors', 'formats', 'qualities' + ]), 'complex_params': { - 'at_contexts': _parse_string_or_array, 'scale_factors': _parse_int_array, 'formats': _parse_noop, # array of str - 'qualities': _parse_noop}, # array of str - 'context': + 'qualities': _parse_noop # array of str + }, + 'api_context': "http://library.stanford.edu/iiif/image-api/1.1/context.json", 'compliance_prefix': "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level", 'compliance_suffix': "", 'protocol': None, - 'required_params': ['identifier', 'width', 'height', 'profile'], - 'property_to_json': - {'at_contexts': '@context', - 'identifier': '@id'} + 'required_params': [ + 'id', 'width', 'height', 'profile' + ], + 'property_to_json': { + 'id': '@id' + } }, '2.0': { - 'params': - ['at_contexts', 'identifier', 'protocol', 'width', 'height', - 'profile', 'sizes', 'tiles', 'service'], + 'params': [ + 'id', 'protocol', 'width', 'height', + 'profile', 'sizes', 'tiles', 'service' + ], # scale_factors isn't in API but used internally - 'array_params': set( - ['sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'qualities', 'supports']), + 'array_params': set([ + 'sizes', 'tiles', 'service', 'scale_factors', 'formats', + 'qualities', 'supports' + ]), 'complex_params': { - 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, - 'service': _parse_service}, - 'context': "http://iiif.io/api/image/2/context.json", + 'service': _parse_service + }, + 'api_context': "http://iiif.io/api/image/2/context.json", 'compliance_prefix': "http://iiif.io/api/image/2/level", 'compliance_suffix': ".json", 'protocol': "http://iiif.io/api/image", - 'required_params': - ['identifier', 'protocol', 'width', 'height', 'profile'], - 'property_to_json': - {'at_contexts': '@context', - 'identifier': '@id'} + 'required_params': [ + 'id', 'protocol', 'width', 'height', 'profile' + ], + 'property_to_json': { + 'id': '@id' + } }, '2.1': { - 'params': - ['at_contexts', 'identifier', 'protocol', 'width', 'height', - 'profile', 'sizes', 'tiles', 'service', - 'attribution', 'logo', 'license'], + 'params': [ + 'id', 'protocol', 'width', 'height', + 'profile', 'sizes', 'tiles', 'service', + 'attribution', 'logo', 'license' + ], # scale_factors isn't in API but used internally - 'array_params': set( - ['sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']), + 'array_params': set([ + 'sizes', 'tiles', 'service', 'scale_factors', 'formats', + 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports' + ]), 'complex_params': { - 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, - 'service': _parse_service}, - 'context': "http://iiif.io/api/image/2/context.json", + 'service': _parse_service + }, + 'api_context': "http://iiif.io/api/image/2/context.json", 'compliance_prefix': "http://iiif.io/api/image/2/level", 'compliance_suffix': ".json", 'protocol': "http://iiif.io/api/image", - 'required_params': - ['identifier', 'protocol', 'width', 'height', 'profile'], - 'property_to_json': - {'at_contexts': '@context', - 'identifier': '@id'} + 'required_params': [ + 'id', 'protocol', 'width', 'height', 'profile' + ], + 'property_to_json': { + 'id': '@id' + } }, '3.0': { - 'params': - ['at_contexts', 'identifier', 'resource_type', 'protocol', - 'width', 'height', 'profile', 'sizes', 'tiles', 'service', - 'attribution', 'logo', 'license'], + 'params': [ + 'id', 'resource_type', 'protocol', + 'width', 'height', 'profile', 'sizes', 'tiles', 'service', + 'attribution', 'logo', 'license' + ], # scale_factors isn't in API but used internally - 'array_params': set( - ['sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']), + 'array_params': set([ + 'sizes', 'tiles', 'service', 'scale_factors', 'formats', + 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports' + ]), 'complex_params': { - 'at_contexts': _parse_string_or_array, 'sizes': _parse_noop, 'tiles': _parse_tiles, 'profile': _parse_profile, - 'service': _parse_service}, - 'context': "http://iiif.io/api/image/3/context.json", + 'service': _parse_service + }, + 'api_context': "http://iiif.io/api/image/3/context.json", 'compliance_prefix': "http://iiif.io/api/image/3/level", 'compliance_suffix': ".json", 'protocol': "http://iiif.io/api/image", - 'required_params': - ['identifier', 'protocol', 'width', 'height', 'profile'], - 'property_to_json': - {'at_contexts': '@context', - 'identifier': 'id', - 'resource_type': 'type'}, - 'fixed_values': - {'resource_type': 'ImageService3'} + 'required_params': [ + 'id', 'protocol', 'width', 'height', 'profile' + ], + 'property_to_json': { + 'resource_type': 'type' + }, + 'fixed_values': { + 'resource_type': 'ImageService3' + } } } @@ -221,7 +233,7 @@ class IIIFInfo(object): def __init__(self, api_version='2.1', profile=None, level=1, conf=None, server_and_prefix='', identifier=None, width=None, height=None, tiles=None, - sizes=None, service=None, id=None, at_contexts=None, + sizes=None, service=None, id=None, # legacy params from 1.1 scale_factors=None, tile_width=None, tile_height=None, # 1.1 and 2.0 @@ -277,7 +289,6 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, self.attribution = attribution self.logo = logo self.license = license - self.at_contexts = at_contexts # defaults from conf dict if provided if (conf): for option in conf: @@ -285,6 +296,18 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, if (id is not None): self.id = id + @property + def context(self): + """Image API JSON-LD @context. + + Context taken from self.contexts, will return the last + one if there are multiple, None if not set. + """ + try: + return self.contexts[-1] + except: + return None + @property def id(self): """id property based on server_and_prefix and identifier.""" @@ -329,11 +352,13 @@ def set_version_info(self, api_version=None): self.params = CONF[api_version]['params'] self.array_params = CONF[api_version]['array_params'] self.complex_params = CONF[api_version]['complex_params'] - for a in ('context', 'compliance_prefix', 'compliance_suffix', + for a in ('api_context', 'compliance_prefix', 'compliance_suffix', 'protocol', 'required_params', 'property_to_json', 'fixed_values'): if (a in CONF[api_version]): self._setattr(a, CONF[api_version][a]) + if (a == 'api_context'): + self.contexts = [CONF[api_version][a]] # Set any fixed values if hasattr(self, 'fixed_values'): for p, v in self.fixed_values.items(): @@ -521,14 +546,17 @@ def as_json(self, validate=True): self.validate() json_dict = {} if (self.api_version > '1.0'): - json_dict['@context'] = self.context + if (len(self.contexts) > 1): + json_dict['@context'] = self.contexts + else: + json_dict['@context'] = self.context params_to_write = set(self.params) - params_to_write.discard('identifier') + params_to_write.discard('id') if (self.identifier): if (self.api_version == '1.0'): json_dict['identifier'] = self.identifier # local id else: - json_dict[self.json_key('identifier')] = self.id # URI + json_dict[self.json_key('id')] = self.id # URI params_to_write.discard('profile') if (self.compliance): if (self.api_version < '2.0'): @@ -587,53 +615,47 @@ def read(self, fh, api_version=None): j = json.load(fh) # must work out API version in order to know how to parse JSON # extract image API specific @context and API version - self.context = None - self.read_property(j, 'at_contexts') if (api_version == '1.0'): # v1.0 did not have a @context so take the version passed in self.api_version = api_version - elif self.at_contexts is not None: - # determine API version from last context - self.context = self.at_contexts[-1] - api_version_read = None - for v in CONF: - if (v > '1.0' and self.context == CONF[v]['context']): - api_version_read = v - break - if api_version_read is None: - raise IIIFInfoContextError( - "Unknown @context, cannot determine API version (%s)" % - (self.context)) - elif api_version is None: - self.api_version = api_version_read - elif api_version != api_version_read: - raise IIIFInfoContextError( - "Expected API version '%s' but got @context for API version '%s'" % - (api_version, api_version_read)) - if self.api_version < '3.0' and len(self.at_contexts) > 1: - raise IIIFInfoContextError("Multiple @contexts not allowed in versions prior to v3.0") - else: # no @context and not 1.0 - if (api_version is None): - raise IIIFInfoContextError("No @context (and no default given)") - self.api_version = api_version + else: + try: + self.contexts = _parse_string_or_array(j['@context']) + print("contexts = %s" % (self.contexts)) + except KeyError: + # no @context and not 1.0 + if (api_version is None): + raise IIIFInfoContextError("No @context (and no default given)") + self.api_version = api_version + else: + # determine API version from last context, pick highest + # API version for a given context by searching highest + # version first (i.e. get 2.1 not 2.0) + api_version_read = None + for v in sorted(CONF.keys(), reverse=True): + if (v > '1.0' and self.context == CONF[v]['api_context']): + api_version_read = v + break + if api_version_read is None: + raise IIIFInfoContextError( + "Unknown @context, cannot determine API version (%s)" % + (self.context)) + elif api_version is None: + self.api_version = api_version_read + elif api_version != api_version_read: + raise IIIFInfoContextError( + "Expected API version '%s' but got @context for API version '%s'" % + (api_version, api_version_read)) + if self.api_version < '3.0' and len(self.contexts) > 1: + raise IIIFInfoContextError("Multiple @contexts not allowed in versions prior to v3.0") self.set_version_info() # # parse remaining JSON top-level keys for param in self.params: - if param != 'at_contexts': - self.read_property(j, param) - # - # @id or identifier (1.0 only) - if (self.api_version == '1.0'): - if ('identifier' in j): - self.id = j['identifier'] - else: - raise IIIFInfoError("Missing identifier in info.json") - else: - id_key = self.json_key('identifier') - if (id_key in j): - self.id = j[id_key] - else: - raise IIIFInfoError("Missing %s in info.json" % (id_key)) + self.read_property(j, param) # + # sanity check for id + id_key = self.json_key('id') + if id_key not in j: + raise IIIFInfoError("Missing %s in info.json" % (id_key)) return True diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 253b214..3f643bd 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -162,7 +162,7 @@ def test13_read_good_context(self): for ctx_file in ['info_good_context1.json', 'info_good_context2.json', 'info_good_context3.json']: - i = IIIFInfo(api_version='3.0') + i = IIIFInfo() i.read(self.open_testdata(ctx_file)) self.assertEqual(i.api_version, '3.0') diff --git a/tests/test_info_common.py b/tests/test_info_common.py index 8a427d2..7fe14ad 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -72,7 +72,7 @@ def test04_json_key(self): self.assertEqual(i.json_key(None), None) self.assertEqual(i.json_key(''), '') self.assertEqual(i.json_key('abc'), 'abc') - self.assertEqual(i.json_key('identifier'), 'id') + self.assertEqual(i.json_key('resource_type'), 'type') def test03_level(self): """Test level handling.""" From adc592cf12f6d25dd3c5c922b353a4bb4987520b Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 20:19:03 +0000 Subject: [PATCH 10/27] Create v3.0 spec examples --- .../info_from_spec_section_5_6.json | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_6.json diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json deleted file mode 100644 index 5bf1c6f..0000000 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_6.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "@context" : "http://iiif.io/api/image/3/context.json", - "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", - "type": "ImageService3", - "protocol" : "http://iiif.io/api/image", - "width" : 6000, - "height" : 4000, - "sizes" : [ - {"width" : 150, "height" : 100}, - {"width" : 600, "height" : 400}, - {"width" : 3000, "height": 2000} - ], - "tiles": [ - {"width" : 512, "scaleFactors" : [1,2,4]}, - {"width" : 1024, "height" : 2048, "scaleFactors" : [8,16]} - ], - "attribution" : [ - { - "@value" : "Provided by Example Organization", - "@language" : "en" - },{ - "@value" : "Darparwyd gan Enghraifft Sefydliad", - "@language" : "cy" - } - ], - "logo" : { - "@id" : "http://example.org/image-service/logo/full/200,/0/default.png", - "service" : { - "@context" : "http://iiif.io/api/image/3/context.json", - "@id" : "http://example.org/image-service/logo", - "profile" : "http://iiif.io/api/image/3/level2.json" - } - }, - "license" : [ - "http://example.org/rights/license1.html", - "https://creativecommons.org/licenses/by/4.0/" - ], - "profile" : [ - "http://iiif.io/api/image/3/level2.json", - { - "formats" : [ "gif", "pdf" ], - "qualities" : [ "color", "gray" ], - "supports" : [ - "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" - ] - } - ], - "service" : [ - { - "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", - "profile": "http://iiif.io/api/annex/service/physdim", - "physicalScale": 0.0025, - "physicalUnits": "in" - },{ - "@context" : "http://geojson.org/contexts/geojson-base.jsonld", - "@id" : "http://www.example.org/geojson/paris.json" - } - ] -} From dd64e28257aed609d41f61d6e1f01a85f43399dc Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 20:19:18 +0000 Subject: [PATCH 11/27] v3.0 spec examples --- iiif/info.py | 160 ++++++++++---- tests/test_info_3_0.py | 202 +++++++++--------- tests/test_info_3_0_auth_services.py | 4 +- .../info_from_spec_section_5_2.json | 24 +-- .../info_from_spec_section_5_3.json | 26 +-- .../info_from_spec_section_5_4.json | 12 ++ .../info_from_spec_section_5_5.json | 12 ++ .../info_from_spec_section_5_7.json | 16 ++ .../info_from_spec_section_5_8.json | 52 +++++ .../info_json_3_0/info_with_extra.json | 18 +- 10 files changed, 337 insertions(+), 189 deletions(-) create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_4.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_5.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_7.json create mode 100644 tests/testdata/info_json_3_0/info_from_spec_section_5_8.json diff --git a/iiif/info.py b/iiif/info.py index 7cc41ee..cb0138d 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -79,16 +79,16 @@ def _parse_profile(json_data): '1.0': { 'params': [ 'id', 'protocol', 'width', 'height', - 'scale_factors', 'tile_width', 'tile_height', 'formats', - 'qualities', 'profile' + 'scale_factors', 'tile_width', 'tile_height', 'extra_formats', + 'extra_qualities', 'profile' ], 'array_params': set([ - 'scale_factors', 'formats', 'qualities' + 'scale_factors', 'extra_formats', 'extra_qualities' ]), 'complex_params': { 'scale_factors': _parse_int_array, - 'formats': _parse_noop, # array of str - 'qualities': _parse_noop # array of str + 'extra_formats': _parse_noop, # array of str + 'extra_qualities': _parse_noop # array of str }, 'compliance_prefix': "http://library.stanford.edu/iiif/image-api/compliance.html#level", @@ -96,22 +96,24 @@ def _parse_profile(json_data): 'protocol': None, 'required_params': ['id', 'width', 'height', 'profile'], 'property_to_json': { - 'id': 'identifier' + 'id': 'identifier', + 'extra_formats': 'formats', + 'extra_qualities': 'qualities' } }, '1.1': { 'params': [ 'id', 'protocol', 'width', 'height', - 'scale_factors', 'tile_width', 'tile_height', 'formats', - 'qualities', 'profile' + 'scale_factors', 'tile_width', 'tile_height', 'extra_formats', + 'extra_qualities', 'profile' ], 'array_params': set([ - 'scale_factors', 'formats', 'qualities' + 'scale_factors', 'extra_formats', 'extra_qualities' ]), 'complex_params': { 'scale_factors': _parse_int_array, - 'formats': _parse_noop, # array of str - 'qualities': _parse_noop # array of str + 'extra_formats': _parse_noop, # array of str + 'extra_qualities': _parse_noop # array of str }, 'api_context': "http://library.stanford.edu/iiif/image-api/1.1/context.json", @@ -123,7 +125,9 @@ def _parse_profile(json_data): 'id', 'width', 'height', 'profile' ], 'property_to_json': { - 'id': '@id' + 'id': '@id', + 'extra_formats': 'formats', + 'extra_qualities': 'qualities' } }, '2.0': { @@ -133,8 +137,8 @@ def _parse_profile(json_data): ], # scale_factors isn't in API but used internally 'array_params': set([ - 'sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'qualities', 'supports' + 'sizes', 'tiles', 'service', 'scale_factors', 'extra_formats', + 'extra_qualities', 'supports' ]), 'complex_params': { 'sizes': _parse_noop, @@ -150,7 +154,9 @@ def _parse_profile(json_data): 'id', 'protocol', 'width', 'height', 'profile' ], 'property_to_json': { - 'id': '@id' + 'id': '@id', + 'extra_formats': 'formats', + 'extra_qualities': 'qualities' } }, '2.1': { @@ -161,8 +167,8 @@ def _parse_profile(json_data): ], # scale_factors isn't in API but used internally 'array_params': set([ - 'sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports' + 'sizes', 'tiles', 'service', 'scale_factors', 'extra_formats', + 'maxArea', 'maxHeight', 'maxWidth', 'extra_qualities', 'supports' ]), 'complex_params': { 'sizes': _parse_noop, @@ -178,19 +184,24 @@ def _parse_profile(json_data): 'id', 'protocol', 'width', 'height', 'profile' ], 'property_to_json': { - 'id': '@id' + 'id': '@id', + 'extra_formats': 'formats', + 'extra_qualities': 'qualities' } }, '3.0': { 'params': [ 'id', 'resource_type', 'protocol', - 'width', 'height', 'profile', 'sizes', 'tiles', 'service', + 'width', 'height', 'profile', 'sizes', 'tiles', + 'extra_formats', 'extra_qualities', 'extra_features', + 'service', 'attribution', 'logo', 'license' ], # scale_factors isn't in API but used internally 'array_params': set([ - 'sizes', 'tiles', 'service', 'scale_factors', 'formats', - 'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports' + 'sizes', 'tiles', 'service', 'scale_factors', + 'extra_formats', 'extra_qualities', 'extra_features', + 'maxArea', 'maxHeight', 'maxWidth', ]), 'complex_params': { 'sizes': _parse_noop, @@ -199,14 +210,17 @@ def _parse_profile(json_data): 'service': _parse_service }, 'api_context': "http://iiif.io/api/image/3/context.json", - 'compliance_prefix': "http://iiif.io/api/image/3/level", - 'compliance_suffix': ".json", + 'compliance_prefix': "level", + 'compliance_suffix': "", 'protocol': "http://iiif.io/api/image", 'required_params': [ 'id', 'protocol', 'width', 'height', 'profile' ], 'property_to_json': { - 'resource_type': 'type' + 'resource_type': 'type', + 'extra_formats': 'extraFormats', + 'extra_qualities': 'extraQualities', + 'extra_features': 'extraFeatures' }, 'fixed_values': { 'resource_type': 'ImageService3' @@ -236,12 +250,15 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, sizes=None, service=None, id=None, # legacy params from 1.1 scale_factors=None, tile_width=None, tile_height=None, - # 1.1 and 2.0 + # 1.1, 2.0, 2.1 formats=None, qualities=None, - # 2.0 only + # 2.0 and 2.1 only supports=None, # 2.1 only - attribution=None, logo=None, license=None + attribution=None, logo=None, license=None, + # 3.0 only + extra_formats=None, extra_qualities=None, + extra_features=None, ): """Initialize an IIIFInfo object. @@ -281,14 +298,21 @@ def __init__(self, api_version='2.1', profile=None, level=1, conf=None, if (tile_height is not None): self.tile_height = tile_height # 1.1+ - self.formats = formats - self.qualities = qualities + self.extra_formats = formats + self.extra_qualities = qualities # 2.0+ only self.supports = supports # 2.1+ only self.attribution = attribution self.logo = logo self.license = license + # 3.0 only + if extra_formats: + self.extra_formats = extra_formats + if extra_qualities: + self.extra_qualities = extra_qualities + if extra_features: + self.extra_features = extra_features # defaults from conf dict if provided if (conf): for option in conf: @@ -308,6 +332,11 @@ def context(self): except: return None + def add_context(self, context): + """Add context to the set of contexts if not already present.""" + if context not in self.contexts: + self.contexts.insert(0, context) + @property def id(self): """id property based on server_and_prefix and identifier.""" @@ -379,27 +408,36 @@ def json_key(self, property): def compliance(self): """Compliance profile URI. + In IIIF Image API v3.x the profile information is a JSON-LD + aliased string representing the compliance level URI. + In IIIF Image API v2.x the profile information is an array of values and objects, the first of which must be the - compliance level URI. In v1.x the profile information is - just this URI. + compliance level URI. + + In IIIF Image API v1.x the profile information is + just a single profile URI. """ - if (self.api_version < '2.0'): + if self.api_version < '2.0': return self.profile - else: + elif self.api_version < '3.0': return self.profile[0] + else: + return self.profile @compliance.setter def compliance(self, value): """Set the compliance profile URI.""" if (self.api_version < '2.0'): self.profile = value - else: + elif self.api_version < '3.0': try: self.profile[0] = value except AttributeError: # handle case where profile not initialized as array self.profile = [value] + else: + self.profile = value @property def level(self): @@ -561,21 +599,23 @@ def as_json(self, validate=True): if (self.compliance): if (self.api_version < '2.0'): json_dict['profile'] = self.compliance - else: + elif (self.api_version < '3.0'): # FIXME - need to support extra profile features json_dict['profile'] = [self.compliance] d = {} - if (self.formats is not None): - d['formats'] = self.formats - if (self.qualities is not None): - d['qualities'] = self.qualities + if (self.extra_formats is not None): + d['formats'] = self.extra_formats + if (self.extra_qualities is not None): + d['qualities'] = self.extra_qualities if (self.supports is not None): - d['supports'] = self.supports + d['supports'] = self.extra_features if (len(d) > 0): json_dict['profile'].append(d) - params_to_write.discard('formats') - params_to_write.discard('qualities') - params_to_write.discard('supports') + params_to_write.discard('extra_formats') + params_to_write.discard('extra_qualities') + params_to_write.discard('extra_features') + else: + json_dict['profile'] = self.profile for param in params_to_write: if (hasattr(self, param) and getattr(self, param) is not None): @@ -585,14 +625,11 @@ def as_json(self, validate=True): def read_property(self, j, param): """Read one property param from JSON j.""" key = self.json_key(param) - print("param=%s key=%s" % (param, key)) if (key in j): value = j[key] - print("got %s %s" % (param, str(value))) if (param in self.complex_params): # use function ref in self.complex_params to parse value = self.complex_params[param](value) - print("set %s %s" % (param, str(value))) self._setattr(param, value) def read(self, fh, api_version=None): @@ -621,7 +658,6 @@ def read(self, fh, api_version=None): else: try: self.contexts = _parse_string_or_array(j['@context']) - print("contexts = %s" % (self.contexts)) except KeyError: # no @context and not 1.0 if (api_version is None): @@ -659,3 +695,33 @@ def read(self, fh, api_version=None): if id_key not in j: raise IIIFInfoError("Missing %s in info.json" % (id_key)) return True + + @property + def formats(self): + "The pre 3.0 formats property tied to extra_formats." + return self.extra_formats + + @formats.setter + def formats(self, value): + "Set pre 3.0 formats by writing to alias extra_formats.""" + self.extra_formats = value + + @property + def qualities(self): + "The pre 3.0 qualities property tied to extra_qualities." + return self.extra_qualities + + @qualities.setter + def qualities(self, value): + "Set pre 3.0 qualitiess by writing to alias extra_qualities.""" + self.extra_qualities = value + + @property + def supports(self): + "The pre 3.0 supports property tied to extra_features." + return self.extra_features + + @supports.setter + def supports(self, value): + "Set pre 3.0 supports by writing to alias extra_features.""" + self.extra_features = value diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 3f643bd..2dba82a 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -16,13 +16,13 @@ def open_testdata(self, filename): def test01_minmal(self): """Trivial JSON test.""" # ?? should this empty case raise and error instead? - ir = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') + ir = IIIFInfo(identifier="https://example.com/i1", api_version='3.0') self.assertJSONEqual(ir.as_json(validate=False), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "https://example.com/i1", \n "profile": "level1", \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') ir.width = 100 ir.height = 200 self.assertJSONEqual(ir.as_json(), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "height": 200, \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", \n"type": "ImageService3",\n "width": 100\n}') + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "height": 200, \n "id": "https://example.com/i1", \n "profile": "level1", \n "protocol": "http://iiif.io/api/image", \n"type": "ImageService3",\n "width": 100\n}') def test04_conf(self): """Tile parameter configuration.""" @@ -39,19 +39,19 @@ def test05_level_and_profile(self): i = IIIFInfo(api_version='3.0') i.level = 0 self.assertEqual(i.level, 0) - self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level0.json") + self.assertEqual(i.compliance, "level0") i.level = 2 self.assertEqual(i.level, 2) - self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json") + self.assertEqual(i.compliance, "level2") # Set via compliance - i.compliance = "http://iiif.io/api/image/3/level1.json" + i.compliance = "level1" self.assertEqual(i.level, 1) # Set via profile - i.profile = ["http://iiif.io/api/image/3/level1.json"] - self.assertEqual(i.level, 1) + i.profile = "level2" + self.assertEqual(i.level, 2) # Set new via compliance i = IIIFInfo(api_version='3.0') - i.compliance = "http://iiif.io/api/image/3/level1.json" + i.compliance = "level1" self.assertEqual(i.level, 1) def test06_validate(self): @@ -65,63 +65,96 @@ def test06_validate(self): def test10_read_examples_from_spec(self): """Test reading of examples from spec.""" - # Section 5.2, full example + # Section 5.2, max dimensions i = IIIFInfo(api_version='3.0') fh = self.open_testdata('info_from_spec_section_5_2.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") self.assertEqual(i.id, - "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.resource_type, "ImageService3") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + # Section 5.3, sizes + i = IIIFInfo(api_version='3.0') + fh = self.open_testdata('info_from_spec_section_5_3.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.resource_type, "ImageService3") self.assertEqual(i.protocol, "http://iiif.io/api/image") self.assertEqual(i.width, 6000) self.assertEqual(i.height, 4000) self.assertEqual(i.sizes, [{"width": 150, "height": 100}, {"width": 600, "height": 400}, {"width": 3000, "height": 2000}]) + self.assertEqual(i.profile, "level2") + self.assertEqual(i.compliance, "level2") + # Section 5.4, tiles + i = IIIFInfo(api_version='3.0') + fh = self.open_testdata('info_from_spec_section_5_4.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.resource_type, "ImageService3") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) self.assertEqual(i.tiles, [{"width": 512, "scaleFactors": [1, 2, 4, 8, 16]}]) - self.assertEqual(i.profile, - ["http://iiif.io/api/image/3/level2.json"]) - # extracted information - self.assertEqual(i.compliance, - "http://iiif.io/api/image/3/level2.json") + self.assertEqual(i.profile, "level2") + self.assertEqual(i.compliance, "level2") # and 1.1 style tile properties self.assertEqual(i.tile_width, 512) self.assertEqual(i.tile_height, 512) self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16]) - - # Section 5.3, full example + # Section 5.5, rights etc. i = IIIFInfo(api_version='3.0') - fh = self.open_testdata('info_from_spec_section_5_3.json') + fh = self.open_testdata('info_from_spec_section_5_5.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") self.assertEqual(i.id, - "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") self.assertEqual(i.protocol, "http://iiif.io/api/image") - self.assertEqual(i.width, 4000) - self.assertEqual(i.height, 3000) - self.assertEqual( - i.profile, - ["http://iiif.io/api/image/3/level2.json", - {"formats": ["gif", "pdf"], - "maxWidth": 2000, - "qualities": ["color", "gray"], - "supports": ["canonicalLinkHeader", "rotationArbitrary", - "profileLinkHeader", "http://example.com/feature/"]}]) - # extracted information - self.assertEqual(i.compliance, - "http://iiif.io/api/image/3/level2.json") - - # Section 5.6, full example + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + self.assertEqual(i.profile, "level2") + self.assertEqual(i.attribution, "Provided by Example Organization") + self.assertEqual(i.logo, "https://example.org/images/logo.png") + self.assertEqual(i.license, "http://rightsstatements.org/vocab/InC-EDU/1.0/") + # Section 5.7, simple service + i = IIIFInfo(api_version='3.0') + fh = self.open_testdata('info_from_spec_section_5_7.json') + i.read(fh) + self.assertEqual(i.context, + "http://iiif.io/api/image/3/context.json") + self.assertEqual(i.id, + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + self.assertEqual(i.protocol, "http://iiif.io/api/image") + self.assertEqual(i.width, 6000) + self.assertEqual(i.height, 4000) + self.assertEqual(i.profile, "level2") + self.assertEqual(i.compliance, "level2") + self.assertEqual(i.service, [{ + "@id": "https://example.org/auth/login.html", + "@type": "AuthCookieService1", + "profile": "http://iiif.io/api/auth/1/login" + }]) + # Section 5.8, full example i = IIIFInfo(api_version='3.0') - fh = self.open_testdata('info_from_spec_section_5_6.json') + fh = self.open_testdata('info_from_spec_section_5_8.json') i.read(fh) self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") self.assertEqual(i.id, - "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") self.assertEqual(i.protocol, "http://iiif.io/api/image") self.assertEqual(i.width, 6000) self.assertEqual(i.height, 4000) @@ -137,7 +170,7 @@ def test11_read_example_with_extra(self): self.assertEqual(i.context, "http://iiif.io/api/image/3/context.json") self.assertEqual(i.id, - "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") + "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C") self.assertEqual(i.protocol, "http://iiif.io/api/image") self.assertEqual(i.width, 6000) self.assertEqual(i.height, 4000) @@ -146,7 +179,7 @@ def test11_read_example_with_extra(self): # and should have 1.1-like params too self.assertEqual(i.tile_width, 512) self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16]) - self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json") + self.assertEqual(i.compliance, "level2") def test12_read_bad_context(self): """Test bad/unknown context.""" @@ -166,11 +199,11 @@ def test13_read_good_context(self): i.read(self.open_testdata(ctx_file)) self.assertEqual(i.api_version, '3.0') - def test20_write_example_in_spec(self): + def test20_write_full_example_in_spec(self): """Create example info.json in spec.""" i = IIIFInfo( api_version='3.0', - id="http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + id="https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", # "protocol" : "http://iiif.io/api/image", width=6000, height=4000, @@ -186,73 +219,42 @@ def test20_write_example_in_spec(self): "@language": "en"}, {"@value": "Darparwyd gan Enghraifft Sefydliad", "@language": "cy"}], - logo={"@id": "http://example.org/image-service/logo/full/200,/0/default.png", + logo={"id": "https://example.org/image-service/logo/square/200,200/0/default.png", "service": - {"@context": "http://iiif.io/api/image/3/context.json", - "@id": "http://example.org/image-service/logo", - "profile": "http://iiif.io/api/image/3/level2.json"}}, + [{"id": "https://example.org/image-service/logo", + "profile": "level2", + 'type': 'ImageService3'}]}, license=[ - "http://example.org/rights/license1.html", - "https://creativecommons.org/licenses/by/4.0/"], - profile=["http://iiif.io/api/image/3/level2.json"], - formats=["gif", "pdf"], - qualities=["color", "gray"], - supports=["canonicalLinkHeader", "rotationArbitrary", - "profileLinkHeader", "http://example.com/feature/"], - service=[ - {"@context": "http://iiif.io/api/annex/service/physdim/1/context.json", - "profile": "http://iiif.io/api/annex/service/physdim", - "physicalScale": 0.0025, - "physicalUnits": "in"}, - {"@context": "http://geojson.org/contexts/geojson-base.jsonld", - "@id": "http://www.example.org/geojson/paris.json"}] + "https://example.org/rights/license1.html", + "http://rightsstatements.org/vocab/InC-EDU/1.0/"], + profile="level1", + extra_formats=["gif", "pdf"], + extra_qualities=["color", "gray"], + extra_features=["canonicalLinkHeader", "rotationArbitrary", + "profileLinkHeader"], + service=[{'id': 'http://example.org/fix/me'}] ) + i.add_context("http://example.org/extension/context1.json") reparsed_json = json.loads(i.as_json()) - example_json = json.load(self.open_testdata('info_from_spec_section_5_6.json')) + example_json = json.load(self.open_testdata('info_from_spec_section_5_8.json')) self.maxDiff = 4000 self.assertEqual(reparsed_json, example_json) def test21_write_profile(self): """Test writing of profile information.""" - i = IIIFInfo( - api_version='3.0', - id="http://example.org/svc/a", width=1, height=2, - profile=['pfl'], formats=["fmt1", "fmt2"]) - j = json.loads(i.as_json()) - self.assertEqual(len(j['profile']), 2) - self.assertEqual(j['profile'][0], 'pfl') - self.assertEqual(j['profile'][1], {'formats': ['fmt1', 'fmt2']}) - i = IIIFInfo( - api_version='3.0', - id="http://example.org/svc/a", width=1, height=2, - profile=['pfl'], qualities=None) - j = json.loads(i.as_json()) - self.assertEqual(len(j['profile']), 1) - self.assertEqual(j['profile'][0], 'pfl') - i = IIIFInfo( - api_version='3.0', - id="http://example.org/svc/a", width=1, height=2, - profile=['pfl'], qualities=['q1', 'q2', 'q0']) - j = json.loads(i.as_json()) - self.assertEqual(len(j['profile']), 2) - self.assertEqual(j['profile'][0], 'pfl') - self.assertEqual(j['profile'][1], {'qualities': ['q1', 'q2', 'q0']}) - i = IIIFInfo( - api_version='3.0', - id="http://example.org/svc/a", width=1, height=2, - profile=['pfl'], supports=['a', 'b']) + i = IIIFInfo(api_version='3.0', + id="https://example.org/svc/a", width=1, height=2, + profile='pfl') j = json.loads(i.as_json()) - self.assertEqual(len(j['profile']), 2) - self.assertEqual(j['profile'][0], 'pfl') - self.assertEqual(j['profile'][1], {'supports': ['a', 'b']}) - i = IIIFInfo( - api_version='3.0', - id="http://example.org/svc/a", width=1, height=2, - profile=['pfl'], formats=["fmt1", "fmt2"], - qualities=['q1', 'q2', 'q0'], supports=['a', 'b']) + self.assertEqual(j['profile'], 'pfl') + + def test22_write_extra_info(self): + i = IIIFInfo(api_version='3.0', + id="https://example.org/svc/a", width=1, height=2, + extra_qualities=['aaa1', 'aaa2']) j = json.loads(i.as_json()) - self.assertEqual(len(j['profile']), 2) - self.assertEqual(j['profile'][0], 'pfl') - self.assertEqual(j['profile'][1]['formats'], ['fmt1', 'fmt2']) - self.assertEqual(j['profile'][1]['qualities'], ['q1', 'q2', 'q0']) - self.assertEqual(j['profile'][1]['supports'], ['a', 'b']) + self.assertEqual(j['profile'], 'level1') + self.assertNotIn('extraFormats', j) + self.assertEqual(j['extraQualities'], ['aaa1', 'aaa2']) + self.assertNotIn('extraFeatures', j) + diff --git a/tests/test_info_3_0_auth_services.py b/tests/test_info_3_0_auth_services.py index 77453cd..734f705 100644 --- a/tests/test_info_3_0_auth_services.py +++ b/tests/test_info_3_0_auth_services.py @@ -17,8 +17,8 @@ def test01_empty_auth_defined(self): info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') auth = IIIFAuth() auth.add_services(info) - self.assertJSONEqual(info.as_json( - validate=False), '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": [\n "http://iiif.io/api/image/3/level1.json"\n ], \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') + self.assertJSONEqual(info.as_json(validate=False), + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": "level1",\n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') self.assertEqual(info.service, None) def test02_just_login(self): diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json index 98055ac..9068595 100644 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_2.json @@ -1,16 +1,12 @@ { - "@context" : "http://iiif.io/api/image/3/context.json", - "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", - "protocol" : "http://iiif.io/api/image", - "width" : 6000, - "height" : 4000, - "sizes" : [ - {"width" : 150, "height" : 100}, - {"width" : 600, "height" : 400}, - {"width" : 3000, "height": 2000} - ], - "tiles": [ - {"width" : 512, "scaleFactors" : [1,2,4,8,16]} - ], - "profile" : [ "http://iiif.io/api/image/3/level2.json" ] + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "maxHeight": 2000, + "maxWidth": 3000, + "maxArea": 4000000 } diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json index 795be96..de0bbaa 100644 --- a/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_3.json @@ -1,18 +1,14 @@ { - "@context" : "http://iiif.io/api/image/3/context.json", - "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", - "protocol" : "http://iiif.io/api/image", - "width" : 4000, - "height" : 3000, - "profile" : [ - "http://iiif.io/api/image/3/level2.json", - { - "formats" : [ "gif", "pdf" ], - "qualities" : [ "color", "gray" ], - "maxWidth" : 2000, - "supports" : [ - "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" - ] - } + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "sizes": [ + { "width": 150, "height": 100 }, + { "width": 600, "height": 400 }, + { "width": 3000, "height": 2000 } ] } \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_4.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_4.json new file mode 100644 index 0000000..d1a0c06 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_4.json @@ -0,0 +1,12 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "tiles": [ + { "width": 512, "scaleFactors": [ 1, 2, 4, 8, 16 ] } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_5.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_5.json new file mode 100644 index 0000000..0691a30 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_5.json @@ -0,0 +1,12 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "attribution": "Provided by Example Organization", + "logo": "https://example.org/images/logo.png", + "license": "http://rightsstatements.org/vocab/InC-EDU/1.0/" +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_7.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_7.json new file mode 100644 index 0000000..09d8bb2 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_7.json @@ -0,0 +1,16 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "service": [ + { + "@id": "https://example.org/auth/login.html", + "@type": "AuthCookieService1", + "profile": "http://iiif.io/api/auth/1/login" + } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_from_spec_section_5_8.json b/tests/testdata/info_json_3_0/info_from_spec_section_5_8.json new file mode 100644 index 0000000..a6bdbe8 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_from_spec_section_5_8.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "http://example.org/extension/context1.json", + "http://iiif.io/api/image/3/context.json" + ], + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level1", + "width": 6000, + "height": 4000, + "sizes": [ + { "width": 150, "height": 100 }, + { "width": 600, "height": 400 }, + { "width": 3000, "height": 2000 } + ], + "tiles": [ + { "width": 512, "scaleFactors": [ 1, 2, 4 ] }, + { "width": 1024, "height": 2048, "scaleFactors": [ 8, 16 ] } + ], + "attribution": [ + { + "@value": "Provided by Example Organization", + "@language": "en" + },{ + "@value": "Darparwyd gan Enghraifft Sefydliad", + "@language": "cy" + } + ], + "logo": { + "id": "https://example.org/image-service/logo/square/200,200/0/default.png", + "service": [ + { + "id": "https://example.org/image-service/logo", + "type": "ImageService3", + "profile": "level2" + } + ] + }, + "license": [ + "https://example.org/rights/license1.html", + "http://rightsstatements.org/vocab/InC-EDU/1.0/" + ], + "extraFormats": [ "gif", "pdf" ], + "extraQualities": [ "color", "gray" ], + "extraFeatures": [ "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader" ], + "service": [ + { + "id": "http://example.org/fix/me" + } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_with_extra.json b/tests/testdata/info_json_3_0/info_with_extra.json index 249ea9f..f3b86b0 100644 --- a/tests/testdata/info_json_3_0/info_with_extra.json +++ b/tests/testdata/info_json_3_0/info_with_extra.json @@ -1,6 +1,6 @@ { "@context" : "http://iiif.io/api/image/3/context.json", - "id" : "http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "id" : "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", "EXTRA1": "SOME EXTRA HERE", "protocol" : "http://iiif.io/api/image", "width" : 6000, @@ -13,17 +13,13 @@ "tiles": [ {"width" : 512, "scaleFactors" : [1,2,4,8,16]} ], - "profile" : [ - "http://iiif.io/api/image/3/level2.json", - { - "formats" : [ "gif", "pdf" ], - "qualities" : [ "color", "gray" ], - "EXTRA2" : [ "SOME", "EXTRA", { "HERE": "AND HERE" } ], - "supports" : [ + "profile" : "level2", + "extraFormats" : [ "gif", "pdf" ], + "extraQualities" : [ "color", "gray" ], + "EXTRA2" : [ "SOME", "EXTRA", { "HERE": "AND HERE" } ], + "extraFeatures" : [ "canonicalLinkHeader", "rotationArbitrary", "profileLinkHeader", "http://example.com/feature/" - ] - } - ], + ], "service" : { "@context": "http://iiif.io/api/annex/service/physdim/1/context.json", "profile": "http://iiif.io/api/annex/service/physdim", From 41200a8ad86bd8ce596a3b319ae776b64d92f2ca Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 20:27:19 +0000 Subject: [PATCH 12/27] extraX property handling --- iiif/info.py | 25 ++++++++++++------------- tests/test_info_3_0.py | 19 +++++++++++++++++-- tests/test_info_3_0_auth_services.py | 3 ++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index cb0138d..56e8999 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -192,16 +192,15 @@ def _parse_profile(json_data): '3.0': { 'params': [ 'id', 'resource_type', 'protocol', - 'width', 'height', 'profile', 'sizes', 'tiles', - 'extra_formats', 'extra_qualities', 'extra_features', - 'service', - 'attribution', 'logo', 'license' + 'width', 'height', 'profile', 'sizes', 'tiles', + 'extra_formats', 'extra_qualities', 'extra_features', + 'service', 'attribution', 'logo', 'license' ], # scale_factors isn't in API but used internally 'array_params': set([ 'sizes', 'tiles', 'service', 'scale_factors', - 'extra_formats', 'extra_qualities', 'extra_features', - 'maxArea', 'maxHeight', 'maxWidth', + 'extra_formats', 'extra_qualities', 'extra_features', + 'maxArea', 'maxHeight', 'maxWidth' ]), 'complex_params': { 'sizes': _parse_noop, @@ -409,7 +408,7 @@ def compliance(self): """Compliance profile URI. In IIIF Image API v3.x the profile information is a JSON-LD - aliased string representing the compliance level URI. + aliased string representing the compliance level URI. In IIIF Image API v2.x the profile information is an array of values and objects, the first of which must be the @@ -698,30 +697,30 @@ def read(self, fh, api_version=None): @property def formats(self): - "The pre 3.0 formats property tied to extra_formats." + """The pre 3.0 formats property tied to extra_formats.""" return self.extra_formats @formats.setter def formats(self, value): - "Set pre 3.0 formats by writing to alias extra_formats.""" + """Set pre 3.0 formats by writing to alias extra_formats.""" self.extra_formats = value @property def qualities(self): - "The pre 3.0 qualities property tied to extra_qualities." + """The pre 3.0 qualities property tied to extra_qualities.""" return self.extra_qualities @qualities.setter def qualities(self, value): - "Set pre 3.0 qualitiess by writing to alias extra_qualities.""" + """Set pre 3.0 qualitiess by writing to alias extra_qualities.""" self.extra_qualities = value @property def supports(self): - "The pre 3.0 supports property tied to extra_features." + """The pre 3.0 supports property tied to extra_features.""" return self.extra_features @supports.setter def supports(self, value): - "Set pre 3.0 supports by writing to alias extra_features.""" + """Set pre 3.0 supports by writing to alias extra_features.""" self.extra_features = value diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 2dba82a..753f23b 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -249,12 +249,27 @@ def test21_write_profile(self): self.assertEqual(j['profile'], 'pfl') def test22_write_extra_info(self): + """Test writing of extraQualities, extraFormats, extraFeatures.""" + i = IIIFInfo(api_version='3.0', + id="https://example.org/svc/a", width=1, height=2, + extra_formats=['fmt1', 'fmt2']) + j = json.loads(i.as_json()) + self.assertEqual(j['extraFormats'], ['fmt1', 'fmt2']) + self.assertNotIn('extraQualities', j) + self.assertNotIn('extraFeatures', j) + # i = IIIFInfo(api_version='3.0', id="https://example.org/svc/a", width=1, height=2, extra_qualities=['aaa1', 'aaa2']) j = json.loads(i.as_json()) - self.assertEqual(j['profile'], 'level1') self.assertNotIn('extraFormats', j) self.assertEqual(j['extraQualities'], ['aaa1', 'aaa2']) self.assertNotIn('extraFeatures', j) - + # + i = IIIFInfo(api_version='3.0', + id="https://example.org/svc/a", width=1, height=2, + extra_features=['feat1', 'http://example.org/feat2']) + j = json.loads(i.as_json()) + self.assertNotIn('extraFormats', j) + self.assertNotIn('extraQualities', j) + self.assertEqual(j['extraFeatures'], ['feat1', 'http://example.org/feat2']) diff --git a/tests/test_info_3_0_auth_services.py b/tests/test_info_3_0_auth_services.py index 734f705..5bca8d5 100644 --- a/tests/test_info_3_0_auth_services.py +++ b/tests/test_info_3_0_auth_services.py @@ -17,7 +17,8 @@ def test01_empty_auth_defined(self): info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0') auth = IIIFAuth() auth.add_services(info) - self.assertJSONEqual(info.as_json(validate=False), + self.assertJSONEqual( + info.as_json(validate=False), '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "http://example.com/i1", \n "profile": "level1",\n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') self.assertEqual(info.service, None) From 34d2962888ed5e25cebc8a88c8927681b7cb39c8 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 21:16:13 +0000 Subject: [PATCH 13/27] Validate profile and sizes --- iiif/info.py | 100 +++++++++++++++++++++++++++-------------- tests/test_info_3_0.py | 22 ++++++--- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index 56e8999..48b953b 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -30,6 +30,20 @@ def _parse_noop(json_data): return json_data +def _parse_sizes(json_data): + # Parse sizes in 2.0 and above. + # + # 3.0 spec: "A list of JSON objects with the height and width properties. + # These sizes specify preferred values to be provided in the w,h syntax of + # the size request parameter for scaled versions of the full image." + if (not isinstance(json_data, list)): + raise IIIFInfoError("The sizes property have a list value") + for obj in json_data: + if (not isinstance(obj, dict) or "width" not in obj or "height" not in obj): + raise IIIFInfoError("Every entry in the sizes property list must have width and height") + return json_data + + def _parse_tile(json_data): # Parse data for a single tile specification. tile = {} @@ -43,8 +57,9 @@ def _parse_tile(json_data): def _parse_tiles(json_data): # Parse tiles array of tile specifications. # - # Expect common case in 2.0 to map to 1.1 idea of tile_width, - # tile_height and scale_factors. This is the case when len()==1. + # Expect common case in 2.0 and above to map to 1.1 idea of + # tile_width, tile_height and scale_factors. This is the case when + # len()==1. tiles = [] if (len(json_data) == 0): raise IIIFInfoError("Empty tiles array property not allowed.") @@ -57,7 +72,16 @@ def _parse_service(json_data): return json_data -def _parse_profile(json_data): +def _parse_profile_3_x(json_data): + # 3.0 spec: "A string indicating the highest compliance level which is + # fully supported by the service. The value must be one of “level0”, + # “level1”, or “level2”." + if (json_data not in ("level0", "level1", "level2")): + raise IIIFInfoError("The value of the profile property must be a level string") + return json_data + + +def _parse_profile_2_x(json_data): # 2.1 spec: "A list of profiles, indicated by either a URI or an # object describing the features supported. The first entry # in the list must be a compliance level URI." @@ -65,11 +89,26 @@ def _parse_profile(json_data): # 2.0 spec: "An array of profiles, indicated by either a URI or # an object describing the features supported. The first entry in # the array must be a compliance level URI, as defined below." - # + if (not isinstance(json_data, list)): + raise IIIFInfoError("The profile property have a list value") + if (not re.match(r'''http://iiif.io/api/image/2/level[012]\.json''', json_data[0])): + raise IIIFInfoError("The first entry in the profile list must be a compliance level URI.") + return json_data + + +def _parse_profile_1_1(json_data): # 1.1 & 1.0 spec: "URI indicating the compliance level supported. # Values as described in Section 8. Compliance Levels" - # - # FIXME - add some validation! + if (not re.match(r'''http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level[012]''', json_data)): + raise IIIFInfoError("The profile property value must be a compliance level URI.") + return json_data + + +def _parse_profile_1_0(json_data): + # 1.1 & 1.0 spec: "URI indicating the compliance level supported. + # Values as described in Section 8. Compliance Levels" + if (not re.match(r'''http://library.stanford.edu/iiif/image-api/compliance.html#level[012]''', json_data)): + raise IIIFInfoError("The profile property value must be a compliance level URI.") return json_data @@ -79,19 +118,19 @@ def _parse_profile(json_data): '1.0': { 'params': [ 'id', 'protocol', 'width', 'height', - 'scale_factors', 'tile_width', 'tile_height', 'extra_formats', - 'extra_qualities', 'profile' + 'scale_factors', 'tile_width', 'tile_height', + 'extra_formats', 'extra_qualities', 'profile' ], 'array_params': set([ 'scale_factors', 'extra_formats', 'extra_qualities' ]), 'complex_params': { + 'profile': _parse_profile_1_0, 'scale_factors': _parse_int_array, 'extra_formats': _parse_noop, # array of str 'extra_qualities': _parse_noop # array of str }, - 'compliance_prefix': - "http://library.stanford.edu/iiif/image-api/compliance.html#level", + 'compliance_prefix': "http://library.stanford.edu/iiif/image-api/compliance.html#level", 'compliance_suffix': "", 'protocol': None, 'required_params': ['id', 'width', 'height', 'profile'], @@ -104,21 +143,20 @@ def _parse_profile(json_data): '1.1': { 'params': [ 'id', 'protocol', 'width', 'height', - 'scale_factors', 'tile_width', 'tile_height', 'extra_formats', - 'extra_qualities', 'profile' + 'scale_factors', 'tile_width', 'tile_height', + 'extra_formats', 'extra_qualities', 'profile' ], 'array_params': set([ 'scale_factors', 'extra_formats', 'extra_qualities' ]), 'complex_params': { + 'profile': _parse_profile_1_1, 'scale_factors': _parse_int_array, 'extra_formats': _parse_noop, # array of str 'extra_qualities': _parse_noop # array of str }, - 'api_context': - "http://library.stanford.edu/iiif/image-api/1.1/context.json", - 'compliance_prefix': - "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level", + 'api_context': "http://library.stanford.edu/iiif/image-api/1.1/context.json", + 'compliance_prefix': "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level", 'compliance_suffix': "", 'protocol': None, 'required_params': [ @@ -143,7 +181,7 @@ def _parse_profile(json_data): 'complex_params': { 'sizes': _parse_noop, 'tiles': _parse_tiles, - 'profile': _parse_profile, + 'profile': _parse_profile_2_x, 'service': _parse_service }, 'api_context': "http://iiif.io/api/image/2/context.json", @@ -173,7 +211,7 @@ def _parse_profile(json_data): 'complex_params': { 'sizes': _parse_noop, 'tiles': _parse_tiles, - 'profile': _parse_profile, + 'profile': _parse_profile_2_x, 'service': _parse_service }, 'api_context': "http://iiif.io/api/image/2/context.json", @@ -205,7 +243,7 @@ def _parse_profile(json_data): 'complex_params': { 'sizes': _parse_noop, 'tiles': _parse_tiles, - 'profile': _parse_profile, + 'profile': _parse_profile_3_x, 'service': _parse_service }, 'api_context': "http://iiif.io/api/image/3/context.json", @@ -468,27 +506,21 @@ def _single_tile_getter(self, param): # Extract param from a single tileset defintion if (self.tiles is None or len(self.tiles) == 0): return None - elif (len(self.tiles) == 1): - return self.tiles[0].get(param, None) else: - raise IIIFInfoError( - "No single %s in the case of multiple tile definitions." % (param)) + return self.tiles[0].get(param, None) def _single_tile_setter(self, param, value): # Set param for a single tileset defintion if (self.tiles is None or len(self.tiles) == 0): self.tiles = [{}] - elif (len(self.tiles) > 1): - raise IIIFInfoError( - "No single %s in the case of multiple tile definitions." % (param)) self.tiles[0][param] = value @property def scale_factors(self): """Access to scale_factors in 1.x. - Also provides the scale factors in 2.0 and greater - provided there is exactly one tiles definition. + Also provides the scale factors in 2.0 and greated for + only the first tile definition. """ return self._single_tile_getter('scaleFactors') @@ -506,8 +538,8 @@ def scale_factors(self, value): def tile_width(self): """Access to tile_width in 1.x. - Also provides the tile_width in 2.0 and greater - provided there is exactly one tiles definition. + Also provides the tile_width in 2.0 and greater for + only the first tile definition. """ return self._single_tile_getter('width') @@ -520,9 +552,9 @@ def tile_width(self, value): def tile_height(self): """Access to tile_height in 1.x. - Also provides the tile_height in 2.0 and greater - provided there is exactly one tiles definition. If - width is set but not height then return that instead. + Also provides the tile_height in 2.0 and greaterfor + only the first tile definition. If width is set but not + height then return that instead. """ h = self._single_tile_getter('height') if (h is None): @@ -682,7 +714,7 @@ def read(self, fh, api_version=None): "Expected API version '%s' but got @context for API version '%s'" % (api_version, api_version_read)) if self.api_version < '3.0' and len(self.contexts) > 1: - raise IIIFInfoContextError("Multiple @contexts not allowed in versions prior to v3.0") + raise IIIFInfoContextError("Multiple top-level @contexts not allowed in versions prior to v3.0") self.set_version_info() # # parse remaining JSON top-level keys diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 753f23b..916c2a1 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -17,20 +17,30 @@ def test01_minmal(self): """Trivial JSON test.""" # ?? should this empty case raise and error instead? ir = IIIFInfo(identifier="https://example.com/i1", api_version='3.0') - self.assertJSONEqual(ir.as_json(validate=False), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "id": "https://example.com/i1", \n "profile": "level1", \n "protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') + self.assertJSONEqual( + ir.as_json(validate=False), + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n ' + '"id": "https://example.com/i1", \n "profile": "level1", \n ' + '"protocol": "http://iiif.io/api/image", "type": "ImageService3"\n}') ir.width = 100 ir.height = 200 - self.assertJSONEqual(ir.as_json(), - '{\n "@context": "http://iiif.io/api/image/3/context.json", \n "height": 200, \n "id": "https://example.com/i1", \n "profile": "level1", \n "protocol": "http://iiif.io/api/image", \n"type": "ImageService3",\n "width": 100\n}') + self.assertJSONEqual( + ir.as_json(), + '{\n "@context": "http://iiif.io/api/image/3/context.json", \n ' + '"height": 200, \n "id": "https://example.com/i1", \n ' + '"profile": "level1", \n "protocol": "http://iiif.io/api/image", \n' + '"type": "ImageService3",\n "width": 100\n}') def test04_conf(self): """Tile parameter configuration.""" - conf = {'tiles': [{'width': 999, 'scaleFactors': [9, 8, 7]}]} + conf = {'tiles': [{'width': 999, 'scaleFactors': [9, 8, 7]}, + {'width': 500, 'height': 600, 'scaleFactors': [1, 2]}]} i = IIIFInfo(api_version='3.0', conf=conf) self.assertEqual(i.tiles[0]['width'], 999) self.assertEqual(i.tiles[0]['scaleFactors'], [9, 8, 7]) - # 1.1 style values + self.assertEqual(i.tiles[1]['width'], 500) + self.assertEqual(i.tiles[1]['scaleFactors'], [1, 2]) + # 1.1 style values look at only first entry self.assertEqual(i.tile_width, 999) self.assertEqual(i.scale_factors, [9, 8, 7]) From fe5d7eca8c204ad1f52644f42615f396f2ec9294 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 21:30:08 +0000 Subject: [PATCH 14/27] Remove nmisnamed files --- tests/testdata/info_json_3_0/info_good_contex2.json | 8 -------- tests/testdata/info_json_3_0/info_good_contex3.json | 11 ----------- 2 files changed, 19 deletions(-) delete mode 100644 tests/testdata/info_json_3_0/info_good_contex2.json delete mode 100644 tests/testdata/info_json_3_0/info_good_contex3.json diff --git a/tests/testdata/info_json_3_0/info_good_contex2.json b/tests/testdata/info_json_3_0/info_good_contex2.json deleted file mode 100644 index a5a646e..0000000 --- a/tests/testdata/info_json_3_0/info_good_contex2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "@context": [ "http://iiif.io/api/image/3/context.json" ], - "id" : "http://example.org/imgABC", - "protocol" : "http://iiif.io/api/image", - "width" : 6000, - "height" : 4000, - "profile" : "level2" -} diff --git a/tests/testdata/info_json_3_0/info_good_contex3.json b/tests/testdata/info_json_3_0/info_good_contex3.json deleted file mode 100644 index a2e7d7f..0000000 --- a/tests/testdata/info_json_3_0/info_good_contex3.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "@context": [ - "http://example.org/some/context.json", - "http://iiif.io/api/image/3/context.json" - ], - "id" : "http://example.org/imgABC", - "protocol" : "http://iiif.io/api/image", - "width" : 6000, - "height" : 4000, - "profile" : "level2" -} From 80933b55b1b28b015d745b78ddfbbe100ab2f7ee Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 22:01:45 +0000 Subject: [PATCH 15/27] Tests --- iiif/info.py | 6 +++--- tests/test_info_3_0.py | 16 ++++++++++++++-- tests/test_info_common.py | 8 ++++++++ .../info_json_3_0/info_bad_profile1.json | 9 +++++++++ .../info_json_3_0/info_bad_profile2.json | 9 +++++++++ .../testdata/info_json_3_0/info_bad_sizes1.json | 10 ++++++++++ .../testdata/info_json_3_0/info_bad_sizes2.json | 13 +++++++++++++ .../testdata/info_json_3_0/info_bad_sizes3.json | 13 +++++++++++++ .../testdata/info_json_3_0/info_bad_sizes4.json | 10 ++++++++++ 9 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 tests/testdata/info_json_3_0/info_bad_profile1.json create mode 100644 tests/testdata/info_json_3_0/info_bad_profile2.json create mode 100644 tests/testdata/info_json_3_0/info_bad_sizes1.json create mode 100644 tests/testdata/info_json_3_0/info_bad_sizes2.json create mode 100644 tests/testdata/info_json_3_0/info_bad_sizes3.json create mode 100644 tests/testdata/info_json_3_0/info_bad_sizes4.json diff --git a/iiif/info.py b/iiif/info.py index 48b953b..10d6598 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -179,7 +179,7 @@ def _parse_profile_1_0(json_data): 'extra_qualities', 'supports' ]), 'complex_params': { - 'sizes': _parse_noop, + 'sizes': _parse_sizes, 'tiles': _parse_tiles, 'profile': _parse_profile_2_x, 'service': _parse_service @@ -209,7 +209,7 @@ def _parse_profile_1_0(json_data): 'maxArea', 'maxHeight', 'maxWidth', 'extra_qualities', 'supports' ]), 'complex_params': { - 'sizes': _parse_noop, + 'sizes': _parse_sizes, 'tiles': _parse_tiles, 'profile': _parse_profile_2_x, 'service': _parse_service @@ -241,7 +241,7 @@ def _parse_profile_1_0(json_data): 'maxArea', 'maxHeight', 'maxWidth' ]), 'complex_params': { - 'sizes': _parse_noop, + 'sizes': _parse_sizes, 'tiles': _parse_tiles, 'profile': _parse_profile_3_x, 'service': _parse_service diff --git a/tests/test_info_3_0.py b/tests/test_info_3_0.py index 916c2a1..6afa8c9 100644 --- a/tests/test_info_3_0.py +++ b/tests/test_info_3_0.py @@ -192,7 +192,7 @@ def test11_read_example_with_extra(self): self.assertEqual(i.compliance, "level2") def test12_read_bad_context(self): - """Test bad/unknown context.""" + """Test bad/unknown info data.""" for ctx_file in ['info_bad_context1.json', 'info_bad_context2.json', 'info_bad_context3.json']: @@ -200,7 +200,7 @@ def test12_read_bad_context(self): fh = self.open_testdata(ctx_file) self.assertRaises(IIIFInfoContextError, i.read, fh) - def test13_read_good_context(self): + def test13_read_good_info_json(self): """Test good context.""" for ctx_file in ['info_good_context1.json', 'info_good_context2.json', @@ -209,6 +209,18 @@ def test13_read_good_context(self): i.read(self.open_testdata(ctx_file)) self.assertEqual(i.api_version, '3.0') + def test14_read_info_json(self): + """Test bad/unknown info data.""" + for ctx_file in ['info_bad_sizes1.json', + 'info_bad_sizes2.json', + 'info_bad_sizes3.json', + 'info_bad_sizes4.json', + 'info_bad_profile1.json', + 'info_bad_profile2.json']: + i = IIIFInfo(api_version='3.0') + fh = self.open_testdata(ctx_file) + self.assertRaises(IIIFInfoError, i.read, fh) + def test20_write_full_example_in_spec(self): """Create example info.json in spec.""" i = IIIFInfo( diff --git a/tests/test_info_common.py b/tests/test_info_common.py index 7fe14ad..ee19849 100644 --- a/tests/test_info_common.py +++ b/tests/test_info_common.py @@ -21,6 +21,14 @@ def test01_bad_api_versions(self): self.assertRaises(IIIFInfoError, IIIFInfo, api_version='4.0') self.assertRaises(IIIFInfoError, IIIFInfo, api_version='goofy') + def test02_context(self): + """Test context method.""" + i = IIIFInfo() + i.contexts = [] + self.assertEqual(i.context, None) + i.contexts = ['first', 'last'] + self.assertEqual(i.context, 'last') + def test02_id(self): """Test identifier handling.""" i = IIIFInfo() diff --git a/tests/testdata/info_json_3_0/info_bad_profile1.json b/tests/testdata/info_json_3_0/info_bad_profile1.json new file mode 100644 index 0000000..2e07c9d --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_profile1.json @@ -0,0 +1,9 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level5", + "width": 6000, + "height": 4000 +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_bad_profile2.json b/tests/testdata/info_json_3_0/info_bad_profile2.json new file mode 100644 index 0000000..9e4d346 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_profile2.json @@ -0,0 +1,9 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": ["level1"], + "width": 6000, + "height": 4000 +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_bad_sizes1.json b/tests/testdata/info_json_3_0/info_bad_sizes1.json new file mode 100644 index 0000000..c66cb84 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_sizes1.json @@ -0,0 +1,10 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "sizes": "not and array!" +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_bad_sizes2.json b/tests/testdata/info_json_3_0/info_bad_sizes2.json new file mode 100644 index 0000000..3dd7943 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_sizes2.json @@ -0,0 +1,13 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "sizes": [ + { "width": 150, "height": 100 }, + { "width": 600 } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_bad_sizes3.json b/tests/testdata/info_json_3_0/info_bad_sizes3.json new file mode 100644 index 0000000..89441d3 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_sizes3.json @@ -0,0 +1,13 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "sizes": [ + { "width": 150, "height": 100, "extension": 1 }, + { "height": 400 } + ] +} \ No newline at end of file diff --git a/tests/testdata/info_json_3_0/info_bad_sizes4.json b/tests/testdata/info_json_3_0/info_bad_sizes4.json new file mode 100644 index 0000000..2a13538 --- /dev/null +++ b/tests/testdata/info_json_3_0/info_bad_sizes4.json @@ -0,0 +1,10 @@ +{ + "@context": "http://iiif.io/api/image/3/context.json", + "id": "https://example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C", + "type": "ImageService3", + "protocol": "http://iiif.io/api/image", + "profile": "level2", + "width": 6000, + "height": 4000, + "sizes": [ "must be object" ] +} \ No newline at end of file From 2e63d15d5fc81ad0ab7e09f036c3ab25763814c3 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Sun, 25 Feb 2018 22:12:20 +0000 Subject: [PATCH 16/27] Fix bad chars in comment --- iiif/info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iiif/info.py b/iiif/info.py index 10d6598..8912233 100644 --- a/iiif/info.py +++ b/iiif/info.py @@ -74,8 +74,8 @@ def _parse_service(json_data): def _parse_profile_3_x(json_data): # 3.0 spec: "A string indicating the highest compliance level which is - # fully supported by the service. The value must be one of “level0”, - # “level1”, or “level2”." + # fully supported by the service. The value must be one of "level0", + # "level1", or "level2"." if (json_data not in ("level0", "level1", "level2")): raise IIIFInfoError("The value of the profile property must be a level string") return json_data From ab3d101d8e69e4756052a44b8377ae67d1755f36 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Mon, 26 Feb 2018 01:43:57 +0000 Subject: [PATCH 17/27] Allow some variation with rotate due to py2/3 Pillow3/4/5 variation --- tests/test_manipulator_pil.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_manipulator_pil.py b/tests/test_manipulator_pil.py index 2ee4471..7bd47ac 100644 --- a/tests/test_manipulator_pil.py +++ b/tests/test_manipulator_pil.py @@ -111,7 +111,11 @@ def test06_do_rotation(self): # rotate m.do_first() self.assertEqual(m.do_rotation(False, 30.0), None) - self.assertEqual(m.image.size, (218, 202)) + # there is variability between Pillow 4 & 5 py2/3, + # so allow some variation + (w, h) = m.image.size + self.assertIn(w, (218, 219)) + self.assertIn(h, (201, 202)) def test07_do_quality(self): """Test quality selection.""" From 5f5799405ec8b597b6b54e87c225c0340934012c Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 15 Aug 2019 01:54:44 +0100 Subject: [PATCH 18/27] Adding support for upscaling --- iiif/manipulator.py | 8 ++++---- iiif/request.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/iiif/manipulator.py b/iiif/manipulator.py index 020277a..55eae6f 100644 --- a/iiif/manipulator.py +++ b/iiif/manipulator.py @@ -248,7 +248,7 @@ def size_to_apply(self): Assumes current image width and height are available in self.width and self.height, and self.request is IIIFRequest object. - Formats are: w, ,h w,h pct:p !w,h full max + Formats are: w, ,h w,h pct:p !w,h full max ^w, ^,h ^w,h Returns (None,None) if no scaling is required. @@ -305,9 +305,9 @@ def size_to_apply(self): code=400, parameter='size', text="Size parameter would result in zero size result image (%d,%d)." % (w, h)) # Below would be test for scaling up image size, this is allowed by spec - # if ( w>self.width or h>self.height ): - # raise IIIFError(code=400,parameter='size', - # text="Size requests scaling up image to larger than orginal.") + if ( (w>self.width or h>self.height) and self.api_version >= '3.0' and not self.request.size_caret): + raise IIIFError(code=400,parameter='size', + text="Size requests scaling up image to larger than orginal.") if (w == self.width and h == self.height): return(None, None) return(w, h) diff --git a/iiif/request.py b/iiif/request.py index 9b7b560..1fd5d8b 100644 --- a/iiif/request.py +++ b/iiif/request.py @@ -103,6 +103,7 @@ def clear(self): self.size_max = False # new in 2.1 self.size_pct = None self.size_bang = None + self.size_caret = None self.size_wh = None # (w,h) self.rotation_mirror = False self.rotation_deg = 0.0 @@ -395,6 +396,9 @@ def parse_size(self, size=None): /w,h/ -> self.size_wh = (w,h) /pct:p/ -> self.size_pct = p /!w,h/ -> self.size_wh = (w,h), self.size_bang = True + /^w,h/ -> self.size_wh = (w,h), self.size_caret = True + /^w,/ -> self.size_wh = (w,None), self.size_caret = True + /^,h/ -> self.size_wh = (None,h), self.size_caret = True Expected use: (w,h) = iiif.size_to_apply(region_w,region_h) @@ -408,9 +412,16 @@ def parse_size(self, size=None): self.size = size self.size_pct = None self.size_bang = False + self.size_caret = False self.size_full = False self.size_wh = (None, None) - if (self.size is None or self.size == 'full'): + if self.size.startswith('^'): + # gmr + # as caret can be used with any combination of features + # set caret to true and then remove it to catch further processing + self.size_caret = True + self.size = self.size[1:] + if (self.size is None or self.size == 'full' and self.api_version < '3.0'): self.size_full = True return elif (self.size == 'max' and self.api_version >= '2.1'): From 30471ec5e3032cc65450bccb246fecdd64263952 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 15 Aug 2019 12:34:35 +0100 Subject: [PATCH 19/27] Fixing profile link header --- iiif/flask_utils.py | 5 ++++- iiif/manipulator.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/iiif/flask_utils.py b/iiif/flask_utils.py index 348ff64..0194b41 100755 --- a/iiif/flask_utils.py +++ b/iiif/flask_utils.py @@ -316,6 +316,7 @@ def image_information_response(self): i.formats = ["jpg", "png"] # FIXME - should come from manipulator if (self.auth): self.auth.add_services(i) + return self.make_response(i.as_json(), headers={"Content-Type": self.json_mime_type}) @@ -369,8 +370,10 @@ def image_request_response(self, path): self.iiif.format = formats[accept] (outfile, mime_type) = self.manipulator.derive(file, self.iiif) # FIXME - find efficient way to serve file with headers + # could this be the answer: https://stackoverflow.com/questions/31554680/how-to-send-header-in-flask-send-file + # currently no headers are sent with the file self.add_compliance_header() - return send_file(outfile, mimetype=mime_type) + return self.make_response(send_file(outfile, mimetype=mime_type)) def error_response(self, e): """Make response for an IIIFError e. diff --git a/iiif/manipulator.py b/iiif/manipulator.py index 55eae6f..169797d 100644 --- a/iiif/manipulator.py +++ b/iiif/manipulator.py @@ -61,6 +61,8 @@ def compliance_uri(self): elif (self.api_version == '2.0' or self.api_version == '2.1'): uri_pattern = r'http://iiif.io/api/image/2/level%d.json' + elif (self.api_version == '3.0'): + uri_pattern = r'http://iiif.io/api/image/3/level%d.json' else: return if (self.compliance_level is None): From 97c2436e74bc9179093812213efcd5def53ae383 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Mon, 19 Aug 2019 16:47:25 +0100 Subject: [PATCH 20/27] Removing python 3.4 support as validator no longer builds. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1d2f186..be870ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" install: From 09f7d3ca356d5b50c70b5fe2257a2e18ad36597e Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Mon, 19 Aug 2019 16:48:06 +0100 Subject: [PATCH 21/27] Adding VIM files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 789751b..3e20a9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .coverage htmlcov *.pyc +*.swp *~ .DS_store .cache From ead0939cfcfd232696194b50502d5a633ca8f504 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Tue, 20 Aug 2019 10:27:07 +0100 Subject: [PATCH 22/27] Removing support for full with 3.0 --- tests/test_request_3_0.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_request_3_0.py b/tests/test_request_3_0.py index 7120b82..4dbe6f4 100644 --- a/tests/test_request_3_0.py +++ b/tests/test_request_3_0.py @@ -168,14 +168,6 @@ def test03_parse_size(self): self.assertFalse(r.size_pct) self.assertTrue(r.size_bang) self.assertEqual(r.size_wh, (5, 6)) - # 'full' - r = IIIFRequest(api_version='3.0') - r.parse_size('full') - self.assertTrue(r.size_full) - self.assertFalse(r.size_max) - self.assertFalse(r.size_pct) - self.assertFalse(r.size_bang) - self.assertEqual(r.size_wh, (None, None)) # 'max' is new in 3.0 r = IIIFRequest(api_version='3.0') r.parse_size('max') From 5ca4c315d24abf2a9de15a9cfa2f6880858f011d Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Tue, 20 Aug 2019 15:55:02 +0100 Subject: [PATCH 23/27] Adding version 3 tests --- run_validate.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/run_validate.sh b/run_validate.sh index 6a2f167..d0cc1f5 100755 --- a/run_validate.sh +++ b/run_validate.sh @@ -51,6 +51,12 @@ if $show_test_name; then fi iiif-validate.py -s localhost:8000 -p 2.0_pil -i 67352ccc-d1b0-11e1-89ae-279075081939 --version=2.0 --level 2 $verbosity ((errors+=$?)) +if $show_test_name; then + echo "Testing PIL manipulator, API version 3.0" +fi +iiif-validate.py -s localhost:8000 -p 3.0_pil -i 67352ccc-d1b0-11e1-89ae-279075081939 --version=3.0 --level 2 $verbosity +((errors+=$?)) + if $test_netpbm; then if $show_test_name; then echo "Testing netpbm manipulator, API version 1.1" From 3ed92c162a7ba237aca5798dd709d809efdd0427 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Wed, 21 Aug 2019 15:42:39 +0100 Subject: [PATCH 24/27] Rebuilding --- run_validate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_validate.sh b/run_validate.sh index d0cc1f5..2e53c01 100755 --- a/run_validate.sh +++ b/run_validate.sh @@ -52,7 +52,7 @@ fi iiif-validate.py -s localhost:8000 -p 2.0_pil -i 67352ccc-d1b0-11e1-89ae-279075081939 --version=2.0 --level 2 $verbosity ((errors+=$?)) if $show_test_name; then - echo "Testing PIL manipulator, API version 3.0" + echo "Testing PIL manipulator, API version 3.0 " fi iiif-validate.py -s localhost:8000 -p 3.0_pil -i 67352ccc-d1b0-11e1-89ae-279075081939 --version=3.0 --level 2 $verbosity ((errors+=$?)) From b05f79213c0a31f2a65466a8d2b60012d387a8ef Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Wed, 21 Aug 2019 16:11:48 +0100 Subject: [PATCH 25/27] Fixing formatting errors --- iiif/manipulator.py | 6 +++--- iiif/request.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/iiif/manipulator.py b/iiif/manipulator.py index 169797d..12af6a5 100644 --- a/iiif/manipulator.py +++ b/iiif/manipulator.py @@ -307,9 +307,9 @@ def size_to_apply(self): code=400, parameter='size', text="Size parameter would result in zero size result image (%d,%d)." % (w, h)) # Below would be test for scaling up image size, this is allowed by spec - if ( (w>self.width or h>self.height) and self.api_version >= '3.0' and not self.request.size_caret): - raise IIIFError(code=400,parameter='size', - text="Size requests scaling up image to larger than orginal.") + if ((w > self.width or h > self.height) and self.api_version >= '3.0' and not self.request.size_caret): + raise IIIFError(code=400, parameter='size', + text="Size requests scaling up image to larger than orginal.") if (w == self.width and h == self.height): return(None, None) return(w, h) diff --git a/iiif/request.py b/iiif/request.py index 1fd5d8b..6d917f3 100644 --- a/iiif/request.py +++ b/iiif/request.py @@ -416,11 +416,10 @@ def parse_size(self, size=None): self.size_full = False self.size_wh = (None, None) if self.size.startswith('^'): - # gmr - # as caret can be used with any combination of features + # as caret can be used with any combination of features # set caret to true and then remove it to catch further processing self.size_caret = True - self.size = self.size[1:] + self.size = self.size[1:] if (self.size is None or self.size == 'full' and self.api_version < '3.0'): self.size_full = True return From 2c5e79d8c290b96fcb696b63270c2c8eb5f7889b Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Wed, 21 Aug 2019 21:22:58 -0400 Subject: [PATCH 26/27] Start changelog for 1.0.7, note removal of 3.4 support --- CHANGES.rst | 7 ++++++- README | 2 +- iiif/_version.py | 2 +- setup.py | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cdad7f4..dfda4f1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,14 @@ iiif changelog ============== +2019-08... v1.0.7 + +- Add support for IIIF Image API v3.0 +- Drop support for python 3.4 + 2018-03-05 v1.0.6 -- Drop support for Python 2.6 +- Drop support for python 2.6 - Use latest version of Pillow (currently 5.0.0) 2018-02-16 v1.0.5 diff --git a/README b/README index fbd1b27..a1893dd 100644 --- a/README +++ b/README @@ -30,7 +30,7 @@ Installation ------------ The library, test server, static file generator are all designed to -work with Python 2.7, 3.4, 3.5 and 3.6. Manual installation is +work with Python 2.7, 3.5 and 3.6. Manual installation is necessary to get the demonstration documentation and examples. **Automatic installation from PyPI** diff --git a/iiif/_version.py b/iiif/_version.py index 3703952..bf01299 100644 --- a/iiif/_version.py +++ b/iiif/_version.py @@ -1,2 +1,2 @@ """Version number for this IIIF Image API library.""" -__version__ = '1.0.6' +__version__ = '1.0.7' diff --git a/setup.py b/setup.py index c70f1cb..6ccb299 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ def run(self): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Internet :: WWW/HTTP", From 1e60949b3169afbe48c50c1f4dd314788ca36f4c Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Wed, 21 Aug 2019 21:58:13 -0400 Subject: [PATCH 27/27] Tidy instructions, fix test link page for v3 in server to use max --- CHANGES.rst | 6 +++--- README | 4 +++- iiif/flask_utils.py | 3 ++- pypi_upload.md | 13 ++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index dfda4f1..7f84d4b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,15 +1,15 @@ iiif changelog ============== -2019-08... v1.0.7 +2019-08-21 v1.0.7 -- Add support for IIIF Image API v3.0 +- Add preliminary support for IIIF Image API v3.0 in iiif_testserver.py - Drop support for python 3.4 2018-03-05 v1.0.6 -- Drop support for python 2.6 - Use latest version of Pillow (currently 5.0.0) +- Drop support for python 2.6 2018-02-16 v1.0.5 diff --git a/README b/README index a1893dd..9bf9d07 100644 --- a/README +++ b/README @@ -15,6 +15,8 @@ Supports the `International Image Interoperability Framework `_: Image API `2.1 `_ (and versions +`3.0 +`_ PRELIMINARY, `2.0 `_, `1.1 @@ -88,7 +90,7 @@ Copyright and License --------------------- iiif library and programs implementing the IIIF API - Copyright (C) 2012--2018 Simeon Warner + Copyright (C) 2012--2019 Simeon Warner This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/iiif/flask_utils.py b/iiif/flask_utils.py index 0194b41..ed4f82d 100755 --- a/iiif/flask_utils.py +++ b/iiif/flask_utils.py @@ -133,12 +133,13 @@ def prefix_index_page(config): if (config.include_osd): body += ' ' body += "\n" + max = 'max' if api_version >= '3.0' else 'full' for identifier in sorted(ids): base = urljoin('/', config.client_prefix + '/' + identifier) body += '%s' % (identifier) info = base + "/info.json" body += '%s' % (info, 'info') - suffix = "full/full/0/%s" % (default) + suffix = "full/%s/0/%s" % (max, default) body += '%s' % (base + '/' + suffix, suffix) if (config.klass_name != 'dummy'): suffix = "full/256,256/0/%s" % (default) diff --git a/pypi_upload.md b/pypi_upload.md index 19a15b3..e4e51dd 100644 --- a/pypi_upload.md +++ b/pypi_upload.md @@ -9,27 +9,26 @@ iiif is at Putting up a new version ------------------------ - 0. Bump version number working branch in iiif/_version.py and check CHANGES.md is up to date - 1. Check format of README: `rst-lint README` - 2. Check all tests good (python setup.py test; py.test) + 1. Bump version number working branch in iiif/_version.py and check CHANGES.md is up to date + 2. Check all tests good (python setup.py test; ./run_validate.sh -n) 3. Check code is up-to-date with github version 4. Check out master and merge in working branch - 5. Check all tests good (python setup.py test; py.test) + 5. Check all tests good (python setup.py test; ./run_validate.sh -n) 6. Make sure master README has correct travis-ci and coveralls icon links for master branch (?branch=master) 7. Check branches are as expected (git branch -a) - 8. Check local build and version reported OK (python setup.py build; sudo python setup.py install) + 8. Check local build and version reported OK (python setup.py install) 9. Check iiif_testserver.py correctly starts server and is accessible from 10. If all checks out OK, tag and push the new version to github with something like: ``` git tag -n1 #...current tags - git tag -a -m "IIIF API v2.0 and v1.1 at level 2" v0.5.1 + git tag -a -m "IIIF Image API reference implementation, 2018-03-05" v1.0.6 git push --tags python setup.py sdist upload ``` - 11. Then check on PyPI at + 11. Check on PyPI at 12. Finally, back on working branch start new version number by editing `iiif/_version.py` and `CHANGES.md`