• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IIIF / presentation-validator / 23321119381

19 Mar 2026 11:04PM UTC coverage: 76.786% (+1.9%) from 74.85%
23321119381

Pull #199

github

glenrobson
Adding schema to package
Pull Request #199: Refactoring and using uv

418 of 509 new or added lines in 9 files covered. (82.12%)

4 existing lines in 1 file now uncovered.

688 of 896 relevant lines covered (76.79%)

3.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.21
/presentation_validator/validator.py
1
from iiif_prezi.loader import ManifestReader
4✔
2
from jsonschema.exceptions import ValidationError
4✔
3
from presentation_validator.model import ValidationResult, ErrorDetail
4✔
4
from presentation_validator.v3 import schemavalidator
4✔
5

6
import requests
4✔
7
from urllib.parse import urlparse
4✔
8
import traceback
4✔
9

10
import json
4✔
11

12
from pyld import jsonld
4✔
13
jsonld.set_document_loader(jsonld.requests_document_loader(timeout=60))
4✔
14

15
IIIF_HEADER = "application/ld+json;profile=http://iiif.io/api/presentation/{version}/context.json"
4✔
16

17
def check_manifest(data, version, url=None, warnings=[]):
4✔
18
    """Check manifest data at version, return JSON."""
19
    if isinstance(data, str):
4✔
20
        try:
4✔
21
            manifest = json.loads(data)
4✔
22
        except json.JSONDecodeError as e:
4✔
23
            result = ValidationResult()
4✔
24
            result.passed = False
4✔
25
            result.error = str(e)
4✔
26
            return result
4✔
27
    else:
28
        manifest = data
4✔
29

30
    result = ValidationResult()
4✔
31

32
    if not version:
4✔
33
        # peak into the json to find the version
34
        context = manifest.get('@context', '')
4✔
35
        if 'http://iiif.io/api/presentation/4/context.json' in context:
4✔
NEW
36
            version = '4.0'
×
37
        elif 'http://iiif.io/api/presentation/3/context.json' in context:
4✔
NEW
38
            version = '3.0'
×
39
        elif 'http://iiif.io/api/presentation/2/context.json' in context:
4✔
40
            version = '2.1'
4✔
41
        else:
NEW
42
            result.passed = False
×
NEW
43
            result.error = "Unable to determine IIIF presentation version from @context"
×
NEW
44
            return result
×
45

46
    # Check if 3.0 if so run through schema rather than this version...
47
    if version == '3.0':
4✔
48
        try:
4✔
49
            result = schemavalidator.validate(manifest, version, url)
4✔
50
        
51
            if url and 'id' in manifest and manifest['id'] != url:
4✔
52
                raise ValidationError(f"The manifest id ({manifest['id']}) should be the same as the URL it is published at ({url}).")
4✔
53
        except ValidationError as e:
4✔
54
            if result.errorList:
4✔
55
                result.errorList.append(ErrorDetail(
4✔
56
                    'Resolve Error',
57
                    str(e),
58
                    '',
59
                    '/id',
60
                    '{ \'id\': \'...\'}',
61
                    e))
62
            else:
NEW
63
                result.passed = False
×
NEW
64
                result.error = str(e)
×
NEW
65
        except Exception as e:    
×
NEW
66
            traceback.print_exc()
×
NEW
67
            result.passed = False
×
NEW
68
            result.error = f'Presentation Validator bug: "{e}". Please create a <a href="https://github.com/IIIF/presentation-validator/issues">Validator Issue</a>, including a link to the manifest.'
×
69
    else:
70
        if isinstance(data, dict):
4✔
71
            data = json.dumps(data, indent=3)
4✔
72

73
        reader = ManifestReader(data, version=version)
4✔
74
        err = None
4✔
75
        try:
4✔
76
            mf = reader.read()
4✔
77
            mf.toJSON()
4✔
78
            if url and mf.id != url:
4✔
NEW
79
                raise ValidationError("Manifest @id ({}) is different to the location where it was retrieved ({})".format(mf.id, url))
×
80
            # Passed!
81
            result.passed = True
4✔
82
        except KeyError as e:    
4✔
NEW
83
            print ('Failed validation due to:')
×
NEW
84
            traceback.print_exc()
×
NEW
85
            err = 'Failed due to KeyError {}, check trace for details'.format(e)
×
NEW
86
            result.passed = False
×
87
        except Exception as e:
4✔
88
            # Failed
89
            print ('Failed validation due to:')
4✔
90
            traceback.print_exc()
4✔
91
            result.passed = False
4✔
92
            err = e
4✔
93

94
        warnings.extend(reader.get_warnings())
4✔
95

96
        result.warnings = warnings
4✔
97
        result.error = str(err)
4✔
98
        result.url = url
4✔
99

100
    return result
4✔
101

102
def fetch_manifest(url, accept, version):
4✔
103
    """
104
    Fetch a manifest from a URL.
105

106
    Args:
107
        url: URL to retrieve.
108
        accept: Whether to send an Accept header requesting a IIIF media type.
109
        version: Requested IIIF Presentation version, used to build the Accept header.
110
    """
111
    accept_header = None
4✔
112
    if accept and version:
4✔
113
        if version in ("2.0", "2.1"):
4✔
NEW
114
            accept_header = IIIF_HEADER.format(version=2)
×
115
        elif version in ("3.0",):
4✔
116
            accept_header = IIIF_HEADER.format(version=3)
4✔
117
        else:
NEW
118
            accept_header = "application/json"
×
119

120
    parsed_url = urlparse(url)
4✔
121
    if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'):
4✔
122
        raise ValueError("URLs must use HTTP or HTTPS")
4✔
123

124
    headers = {
4✔
125
        "User-Agent": "IIIF Validation Service",
126
        "Accept-Encoding": "gzip",
127
    }
128

129
    if accept_header:
4✔
130
        headers["Accept"] = accept_header
4✔
131

132
    response = requests.get(url, headers=headers)
4✔
133
    response.raise_for_status()
4✔
134

135
    warnings = []
4✔
136

137
    ct = response.headers.get("content-type", "")
4✔
138
    cors = response.headers.get("access-control-allow-origin", "")
4✔
139

140
    if not ct.startswith("application/json") and not ct.startswith("application/ld+json"):
4✔
NEW
141
        warnings.append(
×
142
            'URL does not have correct content-type header: got "%s", expected JSON' % ct
143
        )
144

145
    if cors != "*":
4✔
NEW
146
        warnings.append(
×
147
            'URL does not have correct access-control-allow-origin header: got "%s", expected *'
148
            % cors
149
        )
150

151
    content_encoding = response.headers.get("Content-Encoding", "")
4✔
152
    if content_encoding != "gzip":
4✔
153
        warnings.append(
4✔
154
            "The remote server did not use the requested gzip transfer compression, "
155
            "which will slow access. (Content-Encoding: %s)" % content_encoding
156
        )
157
    elif "Accept-Encoding" not in response.headers.get("Vary", ""):
4✔
NEW
158
        warnings.append(
×
159
            "gzip transfer compression is enabled but the Vary header does not include "
160
            "Accept-Encoding, which can cause compatibility issues"
161
        )
162

163
    return response.json(), warnings
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc