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

IIIF / presentation-validator / 23369153415

21 Mar 2026 01:32AM UTC coverage: 77.306% (+2.5%) from 74.85%
23369153415

push

github

web-flow
Merge pull request #199 from IIIF/refactor-2026

Refactoring and using uv

467 of 564 new or added lines in 11 files covered. (82.8%)

4 existing lines in 1 file now uncovered.

729 of 943 relevant lines covered (77.31%)

3.09 hits per line

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

83.33
/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
from presentation_validator.enum import IIIFVersion
4✔
6

7
import requests
4✔
8
from urllib.parse import urlparse
4✔
9
import traceback
4✔
10
from typing import Any
4✔
11

12
import json
4✔
13

14
from pyld import jsonld
4✔
15
jsonld.set_document_loader(jsonld.requests_document_loader(timeout=60))
4✔
16

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

19
def check_manifest(
4✔
20
                data,
21
                version: IIIFVersion,
22
                url: str | None = None,
23
                warnings: list[str] | None = None,
24
            ) -> dict[str, Any]:
25
    """Check manifest data at version, return JSON."""
26
    if warnings is None:
4✔
27
        warnings = []
4✔
28

29
    if isinstance(data, str):
4✔
30
        try:
4✔
31
            manifest = json.loads(data)
4✔
32
        except json.JSONDecodeError as e:
4✔
33
            result = ValidationResult()
4✔
34
            result.passed = False
4✔
35
            result.error = str(e)
4✔
36
            return result
4✔
37
    else:
38
        manifest = data
4✔
39

40
    result = ValidationResult()
4✔
41

42
    if not version:
4✔
43
        # peak into the json to find the version
44
        version = IIIFVersion.from_context(manifest.get('@context', ''))
4✔
45
        
46
    # Check if 3.0 if so run through schema rather than this version...
47
    if version == IIIFVersion.V3_0:
4✔
48
        try:
4✔
49
            result = schemavalidator.validate(manifest, version, url)
4✔
50
        
51
            # Only test ID match if working with remote URLs
52
            if url and url.startswith('http') and 'id' in manifest and manifest['id'] != url:
4✔
53
                raise ValidationError(f"The manifest id ({manifest['id']}) should be the same as the URL it is published at ({url}).")
4✔
54
        except ValidationError as e:
4✔
55
            if result.errorList:
4✔
56
                result.errorList.append(ErrorDetail(
4✔
57
                    'Resolve Error',
58
                    str(e),
59
                    '',
60
                    '/id',
61
                    '{ \'id\': \'...\'}',
62
                    e))
63
            else:
NEW
64
                result.passed = False
×
NEW
65
                result.error = str(e)
×
NEW
66
        except Exception as e:    
×
NEW
67
            traceback.print_exc()
×
NEW
68
            result.passed = False
×
NEW
69
            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.'
×
70
    else:
71
        if isinstance(data, dict):
4✔
72
            data = json.dumps(data, indent=3)
4✔
73

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

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

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

101
    return result
4✔
102

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

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

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

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

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

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

136
    warnings = []
4✔
137

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

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

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

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

164
    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