, and )".format(tag))
592 |
593 | for child_elem in elem:
594 | child_valid = check_html_element(child_elem)
595 | if not child_valid:
596 | return False
597 | return True
598 |
599 | check_html_element(et)
600 |
601 | # Common field definitions.
602 | def id_field(self, value):
603 | """Validate the ``@id`` field of the resource."""
604 | if value.startswith("urn:uuid:"):
605 | id_uuid = value.replace("urn:uuid:", "")
606 | try:
607 | uuid.UUID(id_uuid)
608 | except ValueError:
609 | self.log_error("@id", "Invalid UUID in @id.")
610 | finally:
611 | return value
612 | return self._http_uri_type("@id", value)
613 |
614 | def type_field(self, value):
615 | """Validate the ``@type`` field of the resource."""
616 | raise NotImplemented
617 |
618 | def label_field(self, value):
619 | """Validate the ``label`` field of the resource."""
620 | return self._str_or_val_lang_type("label", value)
621 |
622 | def description_field(self, value):
623 | """Validate the ``description`` field of the resource."""
624 | return self._str_or_val_lang_type("description", value)
625 |
626 | def attribution_field(self, value):
627 | """Validate the ``attribution`` field of the resource."""
628 | return self._str_or_val_lang_type("attribution", value)
629 |
630 | def license_field(self, value):
631 | """Validate the ``license`` field of the resource."""
632 | return self._repeatable_uri_type("license", value)
633 |
634 | def related_field(self, value):
635 | """Validate the ``related`` field of the resource."""
636 | return self._repeatable_uri_type("related", value)
637 |
638 | def rendering_field(self, value):
639 | """Validate the ``rendering`` field of the resource."""
640 | return self._repeatable_uri_type("rendering", value)
641 |
642 | def service_field(self, value):
643 | """Validate the ``service`` field of the resource."""
644 | return self._repeatable_service_type("service", value)
645 |
646 | def seeAlso_field(self, value):
647 | """Validate the ``seeAlso`` field of the resource."""
648 | return self._repeatable_uri_type("seeAlso", value)
649 |
650 | def within_field(self, value):
651 | """Validate the ``within`` field of the resource."""
652 | return self._repeatable_uri_type("within", value)
653 |
654 | def height_field(self, value):
655 | """Validate ``height`` field."""
656 | if not isinstance(value, int):
657 | self.log_error("height", "height must be int.")
658 | return value
659 |
660 | def width_field(self, value):
661 | """Validate ``width`` field."""
662 | if not isinstance(value, int):
663 | self.log_error("width", "width must be int.")
664 | return value
665 |
666 | def metadata_field(self, value):
667 | """Validate the `metadata` field of the resource.
668 |
669 | Recurse into keys/values and checks that they are properly formatted.
670 | """
671 | if not isinstance(value, list):
672 | self.log_error("metadata", "Metadata MUST be a list")
673 | return value
674 |
675 | result = []
676 | with self._temp_path(self._path + ("metadata",)):
677 | for i, m in enumerate(value):
678 | with self._temp_path(self._path + i):
679 | result.append(self._metadata_entry(m))
680 | return result
681 |
682 | def _metadata_entry(self, value):
683 | if not isinstance(value, dict):
684 | self.log_error("value", "Entries must be dictionaries.")
685 | return value
686 | if "label" not in value:
687 | self.log_error("label", "metadata entries must have labels.")
688 | return value
689 | elif "value" not in value:
690 | self.log_error("value", "metadata entries must have values")
691 | return value
692 | else:
693 | return {
694 | 'label': self._str_or_val_lang_type("label", value.get("label")),
695 | 'value': self._str_or_val_lang_type("value", value.get("value"))
696 | }
697 |
698 | def thumbnail_field(self, value):
699 | """Validate the ``thumbnail`` field of the resource."""
700 | return self._general_image_resource("thumbnail", value)
701 |
702 | def logo_field(self, value):
703 | """Validate the ``logo`` field of the resource."""
704 | return self._general_image_resource("logo", value)
705 |
706 | def _general_image_resource(self, field, value):
707 | """Image resource validator for logos and thumbnails. Basic logic is:
708 |
709 | -Check if field is string. If yes, warn that IIIF image service is preferred.
710 | -If a IIIF image service is avaliable, try to validate it.
711 | -Otherwise, check that it's ID is at least a uri.
712 | """
713 |
714 | if isinstance(value, str):
715 | self.log_warning(field, "{} SHOULD be IIIF image service.".format(field))
716 | return self._uri_type(field, value)
717 | if isinstance(value, dict):
718 | service = value.get("service")
719 | if service and service.get("@context") == "http://iiif.io/api/image/2/context.json":
720 | value['service'] = self.ImageContentValidator.service_field(service)
721 | return value
722 | else:
723 | val = self._uri_type(field, value)
724 | self.log_warning(field, "{} SHOULD be IIIF image service.".format(field))
725 | return val
726 | self.log_error(field, "{} type should be string or dict.".format(field))
727 | return value
728 |
729 | def viewing_hint_field(self, value):
730 | """Validate ``viewingHint`` field against ``VIEW_HINTS`` set."""
731 | if value not in self.VIEW_HINTS:
732 | val, errors = self.mute_errors(self._uri_type, "viewingHint", value)
733 | if errors:
734 | self.log_error("viewingHint", "viewingHint '{}' is not valid and not uri.".format(value))
735 | return value
736 |
737 | def viewing_dir_field(self, value):
738 | """Validate ``viewingDir`` field against ``VIEW_DIRS`` set."""
739 | if value not in self.VIEW_DIRS:
740 | self.log_error("viewingDirection", "viewingDirection '{}' is not valid and not uri.".format(value))
741 | return value
742 |
--------------------------------------------------------------------------------
/tripoli/resource_validators/canvas_validator.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | from collections import OrderedDict
22 |
23 | from .base_validator import BaseValidator
24 |
25 |
26 | class CanvasValidator(BaseValidator):
27 | VIEW_HINTS = {'non-paged', 'facing-pages'}
28 |
29 | KNOWN_FIELDS = BaseValidator.COMMON_FIELDS | {"height", "width", "otherContent", "images"}
30 | FORBIDDEN_FIELDS = {"format", "viewingDirection", "navDate", "startCanvas", "first", "last", "total",
31 | "next", "prev", "startIndex", "collections", "manifests", "members", "sequences",
32 | "structures", "canvases", "resources", "ranges"}
33 | REQUIRED_FIELDS = {"label", "@id", "@type", "height", "width"}
34 |
35 | def __init__(self, iiif_validator):
36 | super().__init__(iiif_validator)
37 | self.CanvasSchema = OrderedDict((
38 | ('@id', self.id_field),
39 | ('@type', self.type_field),
40 | ('label', self.label_field),
41 | ('height', self.height_field),
42 | ('width', self.width_field),
43 | ('other_content', self.other_content_field),
44 | ('images', self.images_field)
45 | ))
46 | self.setup()
47 |
48 | def _run_validation(self, **kwargs):
49 | self._check_all_key_constraints("canvas", self._json)
50 | self.canvas_uri = self._json['@id']
51 | return self._compare_dicts(self.CanvasSchema, self._json)
52 |
53 | def _raise_additional_warnings(self, validation_results):
54 | # Canvas should have a thumbnail if it has multiple images.
55 | if len(validation_results.get('images', [])) > 1 and not validation_results.get("thumbnail"):
56 | self.log_warning("thumbnail", "Canvas SHOULD have a thumbnail when there is more than one image")
57 |
58 | def type_field(self, value):
59 | """Assert that ``@type == 'sc:Canvas``"""
60 | if value != "sc:Canvas":
61 | self.log_error("@type", "@type MUST be 'sc:Canvas'.")
62 | return value
63 |
64 | def images_field(self, value):
65 | """Validate ``images`` list.
66 |
67 | Calls a sub-validation procedure handled by the :class:`AnnotationValidator`.
68 | """
69 | if not value or not isinstance(value, list):
70 | self.log_error("images", "'images' MUST be a list.")
71 | return value
72 |
73 | path = self._path + ("images",)
74 | results = []
75 | for i, anno in enumerate(value):
76 | temp_path = path + i
77 | results.append(self._sub_validate(self.AnnotationValidator, anno, temp_path,
78 | canvas_uri=self.canvas_uri))
79 | return results
80 |
81 | def other_content_field(self, value):
82 | """Validate ``otherContent`` field."""
83 | if not isinstance(value, list):
84 | self.log_error("otherContent", "otherContent must be a list.")
85 | return value
86 | return [self._uri_type("otherContent", item['@id']) for item in value]
87 |
--------------------------------------------------------------------------------
/tripoli/resource_validators/image_content_validator.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | from collections import OrderedDict
22 |
23 | from .base_validator import BaseValidator
24 |
25 |
26 | class ImageContentValidator(BaseValidator):
27 | KNOWN_FIELDS = BaseValidator.COMMON_FIELDS | {"@context", "height", "width", "format"}
28 | FORBIDDEN_FIELDS = {"viewingDirection", "navDate", "startCanvas", "first", "last", "total",
29 | "next", "prev", "startIndex", "collections", "manifests", "members",
30 | "sequences", "structures", "canvases", "resources", "otherContent",
31 | "images", "ranges"}
32 | REQUIRED_FIELDS = {'@type', '@id'}
33 |
34 | def __init__(self, iiif_validator):
35 | super().__init__(iiif_validator)
36 | self.ImageContentSchema = OrderedDict((
37 | ('@id', self.id_field),
38 | ('@type', self.type_field),
39 | ('height', self.height_field),
40 | ('width', self.width_field),
41 | ('service', self.service_field)
42 | ))
43 | self.setup()
44 |
45 | def _run_validation(self, **kwargs):
46 | self._check_all_key_constraints("resource", self._json)
47 | return self._compare_dicts(self.ImageContentSchema, self._json)
48 |
49 | def type_field(self, value):
50 | """Warn if ``@type != 'dctypes:Image'``"""
51 | if value != 'dctypes:Image':
52 | self.log_error('@type', "@type MUST be \'dctypes:Image\'")
53 | return value
54 |
55 | def service_field(self, value):
56 | """Validate the image service in this resource."""
57 | with self._temp_path(self._path + ('service',)):
58 | self._check_required_fields("image service", value, ['@id', '@context'])
59 | self._check_recommended_fields("image service", value, ['profile'])
60 | context = value.get("@context")
61 | if context and context != self.IMAGE_API_2:
62 | if context != self.IMAGE_API_1:
63 | self.log_error('@context', "Must reference IIIF image API.")
64 | else:
65 | self.log_warning('@context', "SHOULD upgrade to 2.0 IIIF image service.")
66 | return value
67 |
--------------------------------------------------------------------------------
/tripoli/resource_validators/manifest_validator.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | from collections import OrderedDict
22 |
23 | from .base_validator import BaseValidator
24 |
25 |
26 | class ManifestValidator(BaseValidator):
27 | VIEW_DIRS = ['left-to-right', 'right-to-left',
28 | 'top-to-bottom', 'bottom-to-top']
29 | VIEW_HINTS = ['individuals', 'paged', 'continuous']
30 |
31 | KNOWN_FIELDS = BaseValidator.COMMON_FIELDS | {"viewingDirection", "navDate", "sequences", "structures", "@context"}
32 | FORBIDDEN_FIELDS = {"format", "height", "width", "startCanvas", "first", "last", "total", "next", "prev",
33 | "startIndex", "collections", "manifests", "members", "canvases", "resources", "otherContent",
34 | "images", "ranges"}
35 | REQUIRED_FIELDS = {"label", "@context", "@id", "@type", "sequences"}
36 | RECOMMENDED_FIELDS = {"metadata", "description", "thumbnail"}
37 |
38 | def __init__(self, iiif_validator):
39 | super().__init__(iiif_validator)
40 | self.ManifestSchema = OrderedDict((
41 | ('@context', self.context_field),
42 | ('structures', self.structures_field),
43 | ('sequences', self.sequences_field),
44 | ('viewingDirection', self.viewing_dir_field),
45 | ))
46 | self.setup()
47 |
48 | def _run_validation(self, **kwargs):
49 | self._check_all_key_constraints("manifest", self._json)
50 | return self._compare_dicts(self.ManifestSchema, self._json)
51 |
52 | def type_field(self, value):
53 | """Assert that ``@type`` == ``sc:Manifest``. """
54 | if not value == 'sc:Manifest':
55 | self.log_error("@type", "@type must be 'sc:Manifest'.")
56 | return value
57 |
58 | def context_field(self, value):
59 | """Assert that ``@context`` is the IIIF 2.0 presentation API."""
60 | if isinstance(value, str):
61 | if not value == self.PRESENTATION_API_URI:
62 | self.log_error("@context", "'@context' must be set to '{}'".format(self.PRESENTATION_API_URI))
63 | if isinstance(value, list):
64 | if self.PRESENTATION_API_URI not in value:
65 | self.log_error("@context", "'@context' must be set to '{}'".format(self.PRESENTATION_API_URI))
66 | return value
67 |
68 | def structures_field(self, value):
69 | """Validate the ``structures`` field."""
70 | return value
71 |
72 | def sequences_field(self, value):
73 | """Validate ``sequences`` list for Manifest.
74 |
75 | Checks that at least 1 sequence is embedded.
76 | """
77 | if not isinstance(value, list):
78 | self.log_error("sequences", "'sequences' MUST be a list")
79 | return value
80 |
81 | if len(value) == 0:
82 | self.log_error("sequences", "Manifest requires at least one sequence")
83 | return value
84 |
85 | results = []
86 | path = self._path + ("sequences",)
87 | for i, seq in enumerate(value):
88 | temp_path = path + i
89 | if i == 0:
90 | results.append(self._sub_validate(self.SequenceValidator, seq, temp_path, emb=True))
91 | else:
92 | results.append(self._sub_validate(self.SequenceValidator, seq, temp_path, emb=False))
93 | return results
94 |
--------------------------------------------------------------------------------
/tripoli/resource_validators/sequence_validator.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | from collections import OrderedDict
22 |
23 | from .base_validator import BaseValidator
24 |
25 |
26 | class SequenceValidator(BaseValidator):
27 | VIEW_DIRS = {'left-to-right', 'right-to-left',
28 | 'top-to-bottom', 'bottom-to-top'}
29 | VIEW_HINTS = {'individuals', 'paged', 'continuous'}
30 |
31 | KNOWN_FIELDS = BaseValidator.COMMON_FIELDS | {"viewingDirection", "startCanvas", "canvases"}
32 | FORBIDDEN_FIELDS = {"format", "height", "width", "navDate", "first", "last", "total", "next", "prev",
33 | "startIndex", "collections", "manifests", "sequences", "structures", "resources",
34 | "otherContent", "images", "ranges"}
35 | REQUIRED_FIELDS = {"@type", "canvases"}
36 |
37 | def __init__(self, iiif_validator):
38 | super().__init__(iiif_validator)
39 | self.emb = None
40 | self.EmbSequenceSchema = OrderedDict((
41 | ('@type', self.type_field),
42 | ('@context', self.context_field),
43 | ('@id', self.id_field),
44 | ('startCanvas', self.startCanvas_field),
45 | ('viewingDirection', self.viewing_dir_field),
46 | ('canvases', self.canvases_field),
47 | ))
48 |
49 | self.LinkedSequenceSchema = OrderedDict((
50 | ('@type', self.type_field),
51 | ('@id', self.id_field),
52 | ('canvases', self._canvas_not_allowed)
53 | ))
54 | self.setup()
55 |
56 | def _run_validation(self, **kwargs):
57 | self._check_all_key_constraints("sequence", self._json)
58 | return self._validate_sequence(**kwargs)
59 |
60 | def _canvas_not_allowed(self, value):
61 | return self._not_allowed('canvas', value)
62 |
63 | def _validate_sequence(self, emb=True):
64 | self.emb = emb
65 | if self.emb:
66 | return self._compare_dicts(self.EmbSequenceSchema, self._json)
67 | else:
68 | return self._compare_dicts(self.LinkedSequenceSchema, self._json)
69 |
70 | def _raise_additional_warnings(self, validation_results):
71 | pass
72 |
73 | def type_field(self, value):
74 | """Assert that ``@type`` == ``sc:Sequence``"""
75 | if value != "sc:Sequence":
76 | self.log_error("@type", "@type must be 'sc:Sequence'")
77 | return value
78 |
79 | def context_field(self, value):
80 | """Assert that ``@context`` is the IIIF 2.0 presentation API if it is allowed."""
81 | if self.emb:
82 | self.log_error("@context", "@context field not allowed in embedded sequence.")
83 | return value
84 |
85 | if value != self.PRESENTATION_API_URI:
86 | self.log_error("@context", "unknown context.")
87 | return value
88 |
89 | def startCanvas_field(self, value):
90 | """Validate ``startCanvas`` field."""
91 | canvases = self._json.get('canvases', [])
92 |
93 | if any(True for can in canvases if can.get('@id') == value):
94 | pass
95 | else:
96 | self.log_error("startCanvas", "'startCanvas' MUST refer to the @id of some canvas in this sequence.")
97 |
98 | return value
99 |
100 | def canvases_field(self, value):
101 | """Validate ``canvases`` list for Sequence."""
102 | if not isinstance(value, list):
103 | self.log_error("canvases", "'canvases' MUST be a list.")
104 | return value
105 |
106 | if len(value) < 1:
107 | self.log_error("canvases", "'canvases' MUST have at least one entry")
108 | return value
109 |
110 | path = self._path + ("canvases",)
111 | results = []
112 |
113 | for i, canvas in enumerate(value):
114 | temp_path = path + i
115 | results.append(self._sub_validate(self.CanvasValidator, canvas, temp_path))
116 |
117 | return results
118 |
--------------------------------------------------------------------------------
/tripoli/tripoli.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | import json
22 | import logging
23 |
24 | from .exceptions import FailFastException, TypeParseException
25 | from .mixins import SubValidationMixin
26 | from .validator_logging import ValidatorLogError, ValidatorLog, Path
27 | from .resource_validators import (
28 | ManifestValidator, SequenceValidator, CanvasValidator,
29 | ImageContentValidator, AnnotationValidator)
30 |
31 | __version__ = "2.0.0"
32 |
33 |
34 | class IIIFValidator(SubValidationMixin):
35 | #: Sets whether or not to save tracebacks in warnings/errors.
36 | debug = False
37 |
38 | #: Sets whether or not warnings are logged.
39 | #: Default ``True``.
40 | collect_warnings = True
41 |
42 | #: Sets whether or not errors are logged.
43 | collect_errors = True
44 |
45 | #: When ``True``, validation stops at first error hit (faster).
46 | #: If ``False``, entire document will always be validated.
47 | #:
48 | #: Note: Turning ``fail_fast`` off may cause the validator to raise
49 | #: unexpected exceptions if the the document is grossly invalid
50 | #: (for instance, if an integer is supplied where a list is expected).
51 | fail_fast = True
52 |
53 | #: If ``True``, prints all errors and warnings as they occur.
54 | #: If ``False``, errors and warnings only printed after de-duplication.
55 | verbose = False
56 |
57 | #: If ``True``, only one instance of duplicate logged messages will be saved.
58 | #: If ``False``, all logged messages will be saved.
59 | #:
60 | #: Example: If set to true, then if every canvas has error A, instead
61 | #: of having the errors (Error(A, canvas[0]), Error(A, canvas[1]), ...), you
62 | #: will only get Error(A, canvas[0]) (the first error of type A on a canvas).
63 | unique_logging = True
64 |
65 | def __init__(self, debug=False, collect_warnings=True, collect_errors=True, fail_fast=True,
66 | verbose=False, unique_logging=True):
67 | super().__init__()
68 | self._ManifestValidator = None
69 | self._AnnotationValidator = None
70 | self._CanvasValidator = None
71 | self._SequenceValidator = None
72 | self._ImageContentValidator = None
73 |
74 | self.debug = debug
75 | self.collect_warnings = collect_warnings
76 | self.collect_errors = collect_errors
77 | self.fail_fast = fail_fast
78 | self.verbose = verbose
79 | self.unique_logging = unique_logging
80 |
81 | #: ``logging.getLogger()`` used to print output.
82 | self.logger = logging.getLogger("tripoli")
83 |
84 | #: If corrections were made during validation, the corrected document
85 | #: will be placed here.
86 | corrected_doc = {}
87 |
88 | self._setup_to_validate()
89 |
90 | @property
91 | def ManifestValidator(self):
92 | """An instance of a ManifestValidator."""
93 | return self._ManifestValidator
94 |
95 | @property
96 | def SequenceValidator(self):
97 | """An instance of a SequenceValidator."""
98 | return self._SequenceValidator
99 |
100 | @property
101 | def CanvasValidator(self):
102 | """An instance of a CanvasValidator."""
103 | return self._CanvasValidator
104 |
105 | @property
106 | def AnnotationValidator(self):
107 | """An instance of an AnnotationValidator"""
108 | return self._AnnotationValidator
109 |
110 | @property
111 | def ImageContentValidator(self):
112 | """An instance of an ImageContentValidator"""
113 | return self._ImageContentValidator
114 |
115 | @ManifestValidator.setter
116 | def ManifestValidator(self, value):
117 | self._ManifestValidator = value(self)
118 |
119 | @SequenceValidator.setter
120 | def SequenceValidator(self, value):
121 | self._SequenceValidator = value(self)
122 |
123 | @CanvasValidator.setter
124 | def CanvasValidator(self, value):
125 | self._CanvasValidator = value(self)
126 |
127 | @AnnotationValidator.setter
128 | def AnnotationValidator(self, value):
129 | self._AnnotationValidator = value(self)
130 |
131 | @ImageContentValidator.setter
132 | def ImageContentValidator(self, value):
133 | self._ImageContentValidator = value(self)
134 |
135 | def _setup_to_validate(self):
136 | """Make sure all links to sub validators exist."""
137 | if not self._ManifestValidator:
138 | self._ManifestValidator = ManifestValidator(self)
139 | if not self._AnnotationValidator:
140 | self._AnnotationValidator = AnnotationValidator(self)
141 | if not self._CanvasValidator:
142 | self._CanvasValidator = CanvasValidator(self)
143 | if not self._SequenceValidator:
144 | self._SequenceValidator = SequenceValidator(self)
145 | if not self._ImageContentValidator:
146 | self._ImageContentValidator = ImageContentValidator(self)
147 |
148 | self._TYPE_MAP = {
149 | "sc:Manifest": self._ManifestValidator,
150 | "sc:Sequence": self._SequenceValidator,
151 | "sc:Canvas": self._CanvasValidator,
152 | "oa:Annotation": self._AnnotationValidator
153 | }
154 | self._errors = ValidatorLog(self.unique_logging)
155 | self._warnings = ValidatorLog(self.unique_logging)
156 | self.corrected_doc = {}
157 |
158 | def _set_from_sub(self, sub):
159 | """Set the validation attributes to those of a sub_validator.
160 |
161 | Called after sub_validate'ing with validator sub.
162 |
163 | :param sub: A BaseValidator implementing Validator.
164 | """
165 | self.is_valid = sub.is_valid
166 | self.corrected_doc = sub.corrected_doc
167 |
168 | def _output_logging(self):
169 | """Sends errors and warnings to the logger."""
170 | for err in self.errors:
171 | self.logger.error(err.log_str())
172 | for warn in self.warnings:
173 | self.logger.warning(warn.log_str())
174 |
175 | def _parse_json(self, json_dict):
176 | if isinstance(json_dict, str):
177 | try:
178 | json_dict = json.loads(json_dict)
179 | except ValueError:
180 | self._exit_early("Could not parse json.")
181 | return json_dict
182 |
183 | def _get_validator(self, json_dict):
184 | """Parse json_dict and return the correct validator.
185 |
186 | Raises a TypeParseException if this cannot be done.
187 | """
188 |
189 | doc_type = json_dict.get("@type")
190 | if not doc_type:
191 | self._exit_early("Resource has no @type.")
192 |
193 | validator = self._TYPE_MAP.get(doc_type)
194 | if not validator:
195 | self._exit_early("Unknown @type: '{}'".format(doc_type))
196 | return validator
197 |
198 | def _exit_early(self, msg):
199 | """Log an error with message, set is_valid to false, and raise TypeParseException."""
200 | self._errors.add(ValidatorLogError(msg, Path()))
201 | self.is_valid = False
202 | raise TypeParseException
203 |
204 | def validate(self, json_dict, **kwargs):
205 | """Determine the correct validator and validate a resource.
206 |
207 | :param json_dict: A dict or str of a json resource.
208 | """
209 | self._setup_to_validate()
210 | try:
211 | json_dict = self._parse_json(json_dict)
212 | validator = self._get_validator(json_dict)
213 | except TypeParseException:
214 | self._output_logging()
215 | return
216 |
217 | try:
218 | self._sub_validate(validator, json_dict, path=None, **kwargs)
219 | except FailFastException:
220 | pass
221 | self._set_from_sub(validator)
222 | self._output_logging()
223 |
--------------------------------------------------------------------------------
/tripoli/validator_logging.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Alex Parmentier, Andrew Hankinson
2 |
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 |
10 | # The above copyright notice and this permission notice shall be included in
11 | # all copies or substantial portions of the Software.
12 |
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | # THE SOFTWARE.
20 |
21 | import traceback
22 | from itertools import zip_longest
23 |
24 |
25 | class Path:
26 | """Class representing path within document."""
27 | def __init__(self, path=None):
28 | """ Create a Path.
29 |
30 | :param path: Tuple of strings and ints.
31 | """
32 | self._path = path if path is not None else tuple()
33 | self.__no_index_path = None
34 |
35 | def __eq__(self, other):
36 | return self.__no_index_path == other.__no_index_path
37 |
38 | def __len__(self):
39 | return len(self._path)
40 |
41 | def __str__(self):
42 | return ' @ data[%s]' % ']['.join(map(repr, self._path)) if self._path else ''
43 |
44 | def __repr__(self):
45 | return 'Path({})'.format(", ".join(repr(x) for x in self._path))
46 |
47 | def __iter__(self):
48 | return self._path.__iter__()
49 |
50 | def __add__(self, other):
51 | if isinstance(other, (str, int)):
52 | return Path(self._path + (other,))
53 | if isinstance(other, tuple):
54 | return Path(self._path + other)
55 | if isinstance(other, Path):
56 | return Path(self._path + other._path)
57 | return NotImplemented
58 |
59 | def __hash__(self):
60 | return hash(self._path)
61 |
62 | @property
63 | def no_index_path(self):
64 | if self.__no_index_path is None:
65 | self.__no_index_path = tuple(filter(lambda x: isinstance(x, str), self._path))
66 | return self.__no_index_path
67 |
68 | @property
69 | def path(self):
70 | return self._path
71 |
72 | def no_index_eq(self, other):
73 | """Return true if paths are the same, ignoring indexes.
74 |
75 | Useful for hashing, as we typically only care about storing
76 | one instance of each error/warning per list.
77 | """
78 | return self.no_index_path == other.no_index_path
79 |
80 | def no_index_endswith(self, path):
81 | """Return true is self.no_index_path has ``path`` as a suffix.
82 |
83 | :param path: Either a str-tuple or a Path.
84 | """
85 | if isinstance(path, Path):
86 | path = path.no_index_path
87 | return path == self.no_index_path[-(len(path)):]
88 |
89 |
90 | class ValidatorLog:
91 | """Log which provides unified interface for either set or list like behaviour."""
92 | def __init__(self, unique_logging=True):
93 | self.unique_logging = unique_logging
94 | self._entries = set() if unique_logging else []
95 |
96 | def add(self, log_entry):
97 | """Add an entry to the log.
98 |
99 | :param log_entry: A ValidatorLogEntry to add to self.
100 | """
101 | if isinstance(log_entry, ValidatorLogEntry):
102 | if self.unique_logging:
103 | self._entries.add(log_entry)
104 | else:
105 | self._entries.append(log_entry)
106 | else:
107 | raise TypeError('log_entry must be a ValidatorLogEntry')
108 |
109 | def update(self, log_entry):
110 | """Add all entries from log_entry to self.
111 |
112 | :param log_entry: A ValidatorLog to update from.
113 | """
114 | for entry in log_entry._entries:
115 | self.add(entry)
116 |
117 | def __iter__(self):
118 | return iter(self._entries)
119 |
120 | def __bool__(self):
121 | return bool(self._entries)
122 |
123 |
124 | class ValidatorLogEntry:
125 | """Basic error logging class with comparison behavior for hashing."""
126 |
127 | def __init__(self, msg, path, tb=None):
128 | """
129 |
130 | :param msg: A message associated with the log entry.
131 | :param path: A tuple representing the path where entry was logged.
132 | :param tb: A traceback.extract_stack() list from the point entry was logged.
133 | """
134 |
135 | #: A message associated with the log entry.
136 | self.msg = msg
137 |
138 | #: A tuple representing the path where the entry was created.
139 | self.path = Path(path)
140 |
141 | self._tb = tb if tb else []
142 |
143 | def print_trace(self):
144 | """Print the stored traceback if it exists."""
145 | traceback.print_list(self._tb)
146 |
147 | def path_str(self):
148 | return str(self.path)
149 |
150 | def log_str(self):
151 | return self.msg + self.path_str()
152 |
153 | def __lt__(self, other):
154 | return len(self.path) < len(other.path)
155 |
156 | def __hash__(self):
157 | return hash(self.path.no_index_path) ^ hash(self.msg)
158 |
159 | def __eq__(self, other):
160 | return self.path.no_index_path == other.path.no_index_path\
161 | and self.msg == other.msg
162 |
163 |
164 | class ValidatorLogWarning(ValidatorLogEntry):
165 | """Class to hold and present warnings."""
166 |
167 | def __str__(self):
168 | output = "Warning: {}".format(self.msg)
169 | return output + self.path_str()
170 |
171 | def __repr__(self):
172 | return "ValidatorLogWarning('{}', {})".format(self.msg, self.path)
173 |
174 |
175 | class ValidatorLogError(ValidatorLogEntry):
176 | """Class to hold and present errors."""
177 |
178 | def __str__(self):
179 | output = "Error: {}".format(self.msg)
180 | return output + self.path_str()
181 |
182 | def __repr__(self):
183 | return "ValidatorLogError('{}', {})".format(self.msg, self.path)
184 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | secret_key
2 |
--------------------------------------------------------------------------------
/web/index.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request, jsonify, render_template, session, abort
2 | import tripoli
3 | import requests
4 | import ujson as json
5 |
6 | JSON_TYPE = 0
7 | TEXT_TYPE = 1
8 |
9 | app = Flask(__name__)
10 | app.config['json_encoder'] = json
11 |
12 | with open('secret_key', 'rb') as f:
13 | app.secret_key = f.read()
14 |
15 |
16 | class NetworkError(Exception):
17 | def __index__(self, err):
18 | self.err = err
19 |
20 |
21 | def val_with_content_type(value, template):
22 | """Return either json or text/html with value dict."""
23 | mimes = request.accept_mimetypes
24 | json_score = mimes['application/json'] if 'application/json' in mimes else 0
25 | text_html_score = mimes['text/html'] if 'text/html' in mimes else 0
26 | if json_score > text_html_score:
27 | return jsonify(value)
28 | else:
29 | return render_template(template, **value)
30 |
31 |
32 | def fetch_manifest(manifest_url):
33 | try:
34 | resp = requests.get(manifest_url)
35 | except Exception as e:
36 | raise NetworkError(e)
37 | return resp
38 |
39 |
40 | @app.route('/', methods=['GET'])
41 | def index():
42 | manifest_url = request.args.get('manifest')
43 |
44 | if manifest_url:
45 | return validate_manifest(manifest_url)
46 | else:
47 | return index_get()
48 |
49 |
50 | def index_get():
51 | val = {"message": "GET with query parameter 'manifest' to validate.",
52 | "version": tripoli.tripoli.__version__}
53 | return val_with_content_type(val, 'index.html')
54 |
55 |
56 | def validate_manifest(manifest_url):
57 | if manifest_url:
58 | try:
59 | req = fetch_manifest(manifest_url)
60 | except NetworkError as e:
61 | resp = jsonify({"message": "Encountered network error while requesting '{}'".format(manifest_url)})
62 | resp.status_code = 400
63 | return resp
64 |
65 | if req.status_code < 200 or req.status_code >= 400:
66 | resp = jsonify({"message": "Could not retrieve json at '{}'."
67 | " Server responded with status code {}.".format(manifest_url, req.status_code)})
68 | resp.status_code = 400
69 | return resp
70 |
71 | try:
72 | man = json.loads(req.content)
73 | except Exception as e:
74 | resp = jsonify({"message": "Could not parse json at '{}'".format(manifest_url)})
75 | resp.status_code = 400
76 | return resp
77 |
78 | iv = tripoli.IIIFValidator()
79 | iv.fail_fast = False
80 | iv.logger.setLevel("CRITICAL")
81 | iv.validate(man)
82 |
83 | resp = {"errors": [str(err) for err in sorted(iv.errors)],
84 | "warnings": [str(warn) for warn in sorted(iv.warnings)],
85 | "is_valid": iv.is_valid,
86 | "manifest_url": manifest_url,
87 | "version": tripoli.tripoli.__version__}
88 | return val_with_content_type(resp, 'index.html')
89 |
90 | if __name__ == "__main__":
91 | app.run()
92 |
--------------------------------------------------------------------------------
/web/requirements.txt:
--------------------------------------------------------------------------------
1 | aniso8601==1.1.0
2 | click==6.6
3 | Flask==0.11.1
4 | Flask-RESTful==0.3.5
5 | itsdangerous==0.24
6 | Jinja2==2.8
7 | MarkupSafe==0.23
8 | python-dateutil==2.5.3
9 | pytz==2016.6.1
10 | requests==2.10.0
11 | six==1.10.0
12 | tripoli==1.0
13 | ujson==1.35
14 | Werkzeug==0.15.3
15 | aniso8601==1.1.0
16 | click==6.6
17 | Flask==0.11.1
18 | itsdangerous==0.24
19 | Jinja2==2.8
20 | MarkupSafe==0.23
21 | python-dateutil==2.5.3
22 | pytz==2016.6.1
23 | requests==2.10.0
24 | six==1.10.0
25 | tripoli==1.0
26 | ujson==1.35
27 | Werkzeug==0.15.3
28 |
--------------------------------------------------------------------------------
/web/static/pure.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Pure v0.6.0
3 | Copyright 2014 Yahoo! Inc. All rights reserved.
4 | Licensed under the BSD License.
5 | https://github.com/yahoo/pure/blob/master/LICENSE.md
6 | */
7 | /*!
8 | normalize.css v^3.0 | MIT License | git.io/normalize
9 | Copyright (c) Nicolas Gallagher and Jonathan Neal
10 | */
11 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}
12 |
--------------------------------------------------------------------------------
/web/static/style.css:
--------------------------------------------------------------------------------
1 | .site {
2 | display: flex;
3 | min-height: 100vh;
4 | flex-direction: column;
5 | }
6 |
7 | .site-content {
8 | margin-left: auto;
9 | margin-right: auto;
10 | margin-top: 50px;
11 | width:50em;
12 | flex: 1;
13 | }
14 |
15 | .site-footer
16 | {
17 | margin-left: auto;
18 | margin-right: auto;
19 | margin-top: 30px;
20 | width: 50em;
21 | padding-bottom: 1em;
22 | text-align: center;
23 | }
24 |
25 | body {
26 | color: #555;
27 | font-family: "Helvetica", "Arial", sans-serif;
28 | }
29 |
30 | h1,
31 | h2,
32 | strong {
33 | color: #333;
34 | }
35 |
36 | a {
37 | text-decoration: none;
38 | color: #4078c0;
39 | }
40 | a:visited {
41 | color: #4078c0;
42 | }
43 |
44 | a:hover {
45 | text-decoration: underline;
46 | }
47 |
--------------------------------------------------------------------------------
/web/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Results for: {{ manifest_url }}Validate IIIF Manifests with Tripoli.
11 |
19 | {% if manifest_url %}
20 |
30 | {% for error in errors %}
31 |
37 |