Skip to content

Commit 08bb900

Browse files
committed
Add 3.0 with 2.1 behaviors
1 parent bad698b commit 08bb900

13 files changed

+987
-4
lines changed

iiif/info.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""IIIF Image Information Response.
22
33
Model for IIIF Image API 'Image Information Response'.
4-
Default version is 2.1 but also supports 2.0, 1.1 and 1.0.
4+
Default version is 2.1 but also supports 3.0, 2.0, 1.1 and 1.0.
55
66
Philisophy is to migrate this code forward with new versions
77
of the specification but to keep support for all published
@@ -143,6 +143,27 @@ def _parse_profile(info, json_data):
143143
'protocol': "http://iiif.io/api/image",
144144
'required_params':
145145
['identifier', 'protocol', 'width', 'height', 'profile'],
146+
},
147+
'3.0': {
148+
'params':
149+
['identifier', 'protocol', 'width', 'height',
150+
'profile', 'sizes', 'tiles', 'service',
151+
'attribution', 'logo', 'license'],
152+
# scale_factors isn't in API but used internally
153+
'array_params': set(
154+
['sizes', 'tiles', 'service', 'scale_factors', 'formats',
155+
'maxArea', 'maxHeight', 'maxWidth', 'qualities', 'supports']),
156+
'complex_params': {
157+
'sizes': _parse_noop,
158+
'tiles': _parse_tiles,
159+
'profile': _parse_profile,
160+
'service': _parse_service},
161+
'context': "http://iiif.io/api/image/3/context.json",
162+
'compliance_prefix': "http://iiif.io/api/image/3/level",
163+
'compliance_suffix': ".json",
164+
'protocol': "http://iiif.io/api/image",
165+
'required_params':
166+
['identifier', 'protocol', 'width', 'height', 'profile'],
146167
}
147168
}
148169

iiif_reference_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def get_config(base_dir=''):
3333
p.add('--scale-factors', default='auto',
3434
help="Set of tile scale factors or 'auto' to calculate for each image "
3535
"such that there are tiles up to the full image")
36-
p.add('--api-versions', default='1.0,1.1,2.0,2.1',
36+
p.add('--api-versions', default='1.0,1.1,2.0,2.1,3.0',
3737
help="Set of API versions to support")
3838
args = p.parse_args()
3939

iiif_testserver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_config(base_dir=''):
3636
p.add('--scale-factors', default='auto',
3737
help="Set of tile scale factors or 'auto' to calculate for each image "
3838
"such that there are tiles up to the full image")
39-
p.add('--api-versions', default='1.0,1.1,2.0,2.1',
39+
p.add('--api-versions', default='1.0,1.1,2.0,2.1,3.0',
4040
help="Set of API versions to support")
4141
p.add('--manipulators', default='pil',
4242
help="Set of manipuators to instantiate. May be dummy,netpbm,pil "

tests/test_info_3_0.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""Test code for iiif/info.py for Image API v3.0."""
2+
import unittest
3+
from .testlib.assert_json_equal_mixin import AssertJSONEqual
4+
import json
5+
from iiif.info import IIIFInfo
6+
7+
8+
class TestAll(unittest.TestCase, AssertJSONEqual):
9+
"""Tests."""
10+
11+
def test01_minmal(self):
12+
"""Trivial JSON test."""
13+
# ?? should this empty case raise and error instead?
14+
ir = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
15+
self.assertJSONEqual(ir.as_json(validate=False),
16+
'{\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}')
17+
ir.width = 100
18+
ir.height = 200
19+
self.assertJSONEqual(ir.as_json(),
20+
'{\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}')
21+
22+
def test04_conf(self):
23+
"""Tile parameter configuration."""
24+
conf = {'tiles': [{'width': 999, 'scaleFactors': [9, 8, 7]}]}
25+
i = IIIFInfo(api_version='3.0', conf=conf)
26+
self.assertEqual(i.tiles[0]['width'], 999)
27+
self.assertEqual(i.tiles[0]['scaleFactors'], [9, 8, 7])
28+
# 1.1 style values
29+
self.assertEqual(i.tile_width, 999)
30+
self.assertEqual(i.scale_factors, [9, 8, 7])
31+
32+
def test05_level_and_profile(self):
33+
"""Test level and profile setting."""
34+
i = IIIFInfo(api_version='3.0')
35+
i.level = 0
36+
self.assertEqual(i.level, 0)
37+
self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level0.json")
38+
i.level = 2
39+
self.assertEqual(i.level, 2)
40+
self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json")
41+
# Set via compliance
42+
i.compliance = "http://iiif.io/api/image/3/level1.json"
43+
self.assertEqual(i.level, 1)
44+
# Set via profile
45+
i.profile = ["http://iiif.io/api/image/3/level1.json"]
46+
self.assertEqual(i.level, 1)
47+
# Set new via compliance
48+
i = IIIFInfo(api_version='3.0')
49+
i.compliance = "http://iiif.io/api/image/3/level1.json"
50+
self.assertEqual(i.level, 1)
51+
52+
def test06_validate(self):
53+
"""Test validate method."""
54+
i = IIIFInfo(api_version='3.0')
55+
self.assertRaises(Exception, i.validate, ())
56+
i = IIIFInfo(identifier='a')
57+
self.assertRaises(Exception, i.validate, ())
58+
i = IIIFInfo(identifier='a', width=1, height=2)
59+
self.assertTrue(i.validate())
60+
61+
def test10_read_examples_from_spec(self):
62+
"""Test reading of examples from spec."""
63+
# Section 5.2, full example
64+
i = IIIFInfo(api_version='3.0')
65+
fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_2.json')
66+
i.read(fh)
67+
self.assertEqual(i.context,
68+
"http://iiif.io/api/image/3/context.json")
69+
self.assertEqual(i.id,
70+
"http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C")
71+
self.assertEqual(i.protocol, "http://iiif.io/api/image")
72+
self.assertEqual(i.width, 6000)
73+
self.assertEqual(i.height, 4000)
74+
self.assertEqual(i.sizes, [{"width": 150, "height": 100},
75+
{"width": 600, "height": 400},
76+
{"width": 3000, "height": 2000}])
77+
self.assertEqual(i.tiles, [{"width": 512,
78+
"scaleFactors": [1, 2, 4, 8, 16]}])
79+
self.assertEqual(i.profile,
80+
["http://iiif.io/api/image/3/level2.json"])
81+
# extracted information
82+
self.assertEqual(i.compliance,
83+
"http://iiif.io/api/image/3/level2.json")
84+
# and 1.1 style tile properties
85+
self.assertEqual(i.tile_width, 512)
86+
self.assertEqual(i.tile_height, 512)
87+
self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16])
88+
89+
# Section 5.3, full example
90+
i = IIIFInfo(api_version='3.0')
91+
fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_3.json')
92+
i.read(fh)
93+
self.assertEqual(i.context,
94+
"http://iiif.io/api/image/3/context.json")
95+
self.assertEqual(i.id,
96+
"http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C")
97+
self.assertEqual(i.protocol, "http://iiif.io/api/image")
98+
self.assertEqual(i.width, 4000)
99+
self.assertEqual(i.height, 3000)
100+
self.assertEqual(
101+
i.profile,
102+
["http://iiif.io/api/image/3/level2.json",
103+
{"formats": ["gif", "pdf"],
104+
"maxWidth": 2000,
105+
"qualities": ["color", "gray"],
106+
"supports": ["canonicalLinkHeader", "rotationArbitrary",
107+
"profileLinkHeader", "http://example.com/feature/"]}])
108+
# extracted information
109+
self.assertEqual(i.compliance,
110+
"http://iiif.io/api/image/3/level2.json")
111+
112+
# Section 5.6, full example
113+
i = IIIFInfo(api_version='3.0')
114+
fh = open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json')
115+
i.read(fh)
116+
self.assertEqual(i.context,
117+
"http://iiif.io/api/image/3/context.json")
118+
self.assertEqual(i.id,
119+
"http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C")
120+
self.assertEqual(i.protocol, "http://iiif.io/api/image")
121+
self.assertEqual(i.width, 6000)
122+
self.assertEqual(i.height, 4000)
123+
self.assertEqual(i.sizes, [{"width": 150, "height": 100},
124+
{"width": 600, "height": 400},
125+
{"width": 3000, "height": 2000}])
126+
127+
def test11_read_example_with_extra(self):
128+
"""Test read of exampe with extra info."""
129+
i = IIIFInfo(api_version='3.0')
130+
fh = open('tests/testdata/info_json_3_0/info_with_extra.json')
131+
i.read(fh)
132+
self.assertEqual(i.context,
133+
"http://iiif.io/api/image/3/context.json")
134+
self.assertEqual(i.id,
135+
"http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C")
136+
self.assertEqual(i.protocol, "http://iiif.io/api/image")
137+
self.assertEqual(i.width, 6000)
138+
self.assertEqual(i.height, 4000)
139+
self.assertEqual(
140+
i.tiles, [{"width": 512, "scaleFactors": [1, 2, 4, 8, 16]}])
141+
# and should have 1.1-like params too
142+
self.assertEqual(i.tile_width, 512)
143+
self.assertEqual(i.scale_factors, [1, 2, 4, 8, 16])
144+
self.assertEqual(i.compliance, "http://iiif.io/api/image/3/level2.json")
145+
146+
def test12_read_unknown_context(self):
147+
"""Test bad/unknown context."""
148+
i = IIIFInfo(api_version='3.0')
149+
fh = open('tests/testdata/info_json_3_0/info_bad_context.json')
150+
self.assertRaises(Exception, i.read, fh)
151+
152+
def test20_write_example_in_spec(self):
153+
"""Create example info.json in spec."""
154+
i = IIIFInfo(
155+
api_version='3.0',
156+
id="http://www.example.org/image-service/abcd1234/1E34750D-38DB-4825-A38A-B60A345E591C",
157+
# "protocol" : "http://iiif.io/api/image",
158+
width=6000,
159+
height=4000,
160+
sizes=[
161+
{"width": 150, "height": 100},
162+
{"width": 600, "height": 400},
163+
{"width": 3000, "height": 2000}],
164+
tiles=[
165+
{"width": 512, "scaleFactors": [1, 2, 4]},
166+
{"width": 1024, "height": 2048, "scaleFactors": [8, 16]}],
167+
attribution=[
168+
{"@value": "<span>Provided by Example Organization</span>",
169+
"@language": "en"},
170+
{"@value": "<span>Darparwyd gan Enghraifft Sefydliad</span>",
171+
"@language": "cy"}],
172+
logo={"@id": "http://example.org/image-service/logo/full/200,/0/default.png",
173+
"service":
174+
{"@context": "http://iiif.io/api/image/3/context.json",
175+
"@id": "http://example.org/image-service/logo",
176+
"profile": "http://iiif.io/api/image/3/level2.json"}},
177+
license=[
178+
"http://example.org/rights/license1.html",
179+
"https://creativecommons.org/licenses/by/4.0/"],
180+
profile=["http://iiif.io/api/image/3/level2.json"],
181+
formats=["gif", "pdf"],
182+
qualities=["color", "gray"],
183+
supports=["canonicalLinkHeader", "rotationArbitrary",
184+
"profileLinkHeader", "http://example.com/feature/"],
185+
service=[
186+
{"@context": "http://iiif.io/api/annex/service/physdim/1/context.json",
187+
"profile": "http://iiif.io/api/annex/service/physdim",
188+
"physicalScale": 0.0025,
189+
"physicalUnits": "in"},
190+
{"@context": "http://geojson.org/contexts/geojson-base.jsonld",
191+
"@id": "http://www.example.org/geojson/paris.json"}]
192+
)
193+
reparsed_json = json.loads(i.as_json())
194+
example_json = json.load(
195+
open('tests/testdata/info_json_3_0/info_from_spec_section_5_6.json'))
196+
self.maxDiff = 4000
197+
self.assertEqual(reparsed_json, example_json)
198+
199+
def test21_write_profile(self):
200+
"""Test writing of profile information."""
201+
i = IIIFInfo(
202+
api_version='3.0',
203+
id="http://example.org/svc/a", width=1, height=2,
204+
profile=['pfl'], formats=["fmt1", "fmt2"])
205+
j = json.loads(i.as_json())
206+
self.assertEqual(len(j['profile']), 2)
207+
self.assertEqual(j['profile'][0], 'pfl')
208+
self.assertEqual(j['profile'][1], {'formats': ['fmt1', 'fmt2']})
209+
i = IIIFInfo(
210+
api_version='3.0',
211+
id="http://example.org/svc/a", width=1, height=2,
212+
profile=['pfl'], qualities=None)
213+
j = json.loads(i.as_json())
214+
self.assertEqual(len(j['profile']), 1)
215+
self.assertEqual(j['profile'][0], 'pfl')
216+
i = IIIFInfo(
217+
api_version='3.0',
218+
id="http://example.org/svc/a", width=1, height=2,
219+
profile=['pfl'], qualities=['q1', 'q2', 'q0'])
220+
j = json.loads(i.as_json())
221+
self.assertEqual(len(j['profile']), 2)
222+
self.assertEqual(j['profile'][0], 'pfl')
223+
self.assertEqual(j['profile'][1], {'qualities': ['q1', 'q2', 'q0']})
224+
i = IIIFInfo(
225+
api_version='3.0',
226+
id="http://example.org/svc/a", width=1, height=2,
227+
profile=['pfl'], supports=['a', 'b'])
228+
j = json.loads(i.as_json())
229+
self.assertEqual(len(j['profile']), 2)
230+
self.assertEqual(j['profile'][0], 'pfl')
231+
self.assertEqual(j['profile'][1], {'supports': ['a', 'b']})
232+
i = IIIFInfo(
233+
api_version='3.0',
234+
id="http://example.org/svc/a", width=1, height=2,
235+
profile=['pfl'], formats=["fmt1", "fmt2"],
236+
qualities=['q1', 'q2', 'q0'], supports=['a', 'b'])
237+
j = json.loads(i.as_json())
238+
self.assertEqual(len(j['profile']), 2)
239+
self.assertEqual(j['profile'][0], 'pfl')
240+
self.assertEqual(j['profile'][1]['formats'], ['fmt1', 'fmt2'])
241+
self.assertEqual(j['profile'][1]['qualities'], ['q1', 'q2', 'q0'])
242+
self.assertEqual(j['profile'][1]['supports'], ['a', 'b'])

tests/test_info_3_0_auth_services.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Test code for iiif/info.py for Image API v3.0 auth service descriptions.
2+
3+
See: https://github.com/IIIF/iiif.io/blob/image-auth/source/api/image/3.0/authentication.md
4+
"""
5+
import unittest
6+
from .testlib.assert_json_equal_mixin import AssertJSONEqual
7+
import json
8+
from iiif.info import IIIFInfo
9+
from iiif.auth import IIIFAuth
10+
11+
12+
class TestAll(unittest.TestCase, AssertJSONEqual):
13+
"""Tests."""
14+
15+
def test01_empty_auth_defined(self):
16+
"""Test empty auth."""
17+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
18+
auth = IIIFAuth()
19+
auth.add_services(info)
20+
self.assertJSONEqual(info.as_json(
21+
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}')
22+
self.assertEqual(info.service, None)
23+
24+
def test02_just_login(self):
25+
"""Test just login."""
26+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
27+
auth = IIIFAuth()
28+
auth.login_uri = 'http://example.com/login'
29+
auth.add_services(info)
30+
self.assertEqual(info.service['@id'], "http://example.com/login")
31+
self.assertEqual(info.service['label'], "Login to image server")
32+
self.assertEqual(info.service['profile'],
33+
"http://iiif.io/api/auth/1/login")
34+
35+
def test03_login_and_logout(self):
36+
"""Test login and logout."""
37+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
38+
auth = IIIFAuth()
39+
auth.login_uri = 'http://example.com/login'
40+
auth.logout_uri = 'http://example.com/logout'
41+
auth.add_services(info)
42+
self.assertEqual(info.service['@id'], "http://example.com/login")
43+
self.assertEqual(info.service['label'], "Login to image server")
44+
self.assertEqual(info.service['profile'],
45+
"http://iiif.io/api/auth/1/login")
46+
svcs = info.service['service']
47+
self.assertEqual(svcs['@id'], "http://example.com/logout")
48+
self.assertEqual(svcs['label'], "Logout from image server")
49+
self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/logout")
50+
51+
def test04_login_and_client_id(self):
52+
"""Test login and client id."""
53+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
54+
auth = IIIFAuth()
55+
auth.login_uri = 'http://example.com/login'
56+
auth.client_id_uri = 'http://example.com/client_id'
57+
auth.add_services(info)
58+
self.assertEqual(info.service['@id'], "http://example.com/login")
59+
self.assertEqual(info.service['label'], "Login to image server")
60+
self.assertEqual(info.service['profile'],
61+
"http://iiif.io/api/auth/1/login")
62+
svcs = info.service['service']
63+
self.assertEqual(svcs['@id'], "http://example.com/client_id")
64+
self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/clientId")
65+
66+
def test05_login_and_access_token(self):
67+
"""Test login and access token."""
68+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
69+
auth = IIIFAuth()
70+
auth.login_uri = 'http://example.com/login'
71+
auth.access_token_uri = 'http://example.com/token'
72+
auth.add_services(info)
73+
self.assertEqual(info.service['@id'], "http://example.com/login")
74+
self.assertEqual(info.service['label'], "Login to image server")
75+
self.assertEqual(info.service['profile'],
76+
"http://iiif.io/api/auth/1/login")
77+
svcs = info.service['service']
78+
self.assertEqual(svcs['@id'], "http://example.com/token")
79+
self.assertEqual(svcs['profile'], "http://iiif.io/api/auth/1/token")
80+
81+
def test06_full_set(self):
82+
"""Test full set of auth services."""
83+
info = IIIFInfo(identifier="http://example.com/i1", api_version='3.0')
84+
auth = IIIFAuth()
85+
auth.name = "Whizzo!"
86+
auth.logout_uri = 'http://example.com/logout'
87+
auth.access_token_uri = 'http://example.com/token'
88+
auth.client_id_uri = 'http://example.com/clientId'
89+
auth.login_uri = 'http://example.com/login'
90+
auth.add_services(info)
91+
self.assertEqual(info.service['@id'], "http://example.com/login")
92+
self.assertEqual(info.service['label'], "Login to Whizzo!")
93+
svcs = info.service['service']
94+
self.assertEqual(svcs[0]['@id'], "http://example.com/logout")
95+
self.assertEqual(svcs[1]['@id'], "http://example.com/clientId")
96+
self.assertEqual(svcs[2]['@id'], "http://example.com/token")

0 commit comments

Comments
 (0)