Skip to content

Commit 4b9db86

Browse files
authored
Merge pull request #1 from p1c2u/initial
Initial version
2 parents a445a19 + 7629c17 commit 4b9db86

File tree

21 files changed

+1996
-2
lines changed

21 files changed

+1996
-2
lines changed

.travis.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
language: python
2+
sudo: false
3+
matrix:
4+
include:
5+
- python: 2.7
6+
- python: 3.2
7+
- python: 3.3
8+
- python: 3.4
9+
- python: 3.5
10+
- python: 3.6
11+
- python: nightly
12+
- python: pypy
13+
- python: pypy3
14+
allow_failures:
15+
- python: 3.2
16+
- python: nightly
17+
before_install:
18+
- if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install 'coverage<4.0.0'; fi
19+
- pip install codecov
20+
install:
21+
- pip install -e .
22+
- pip install -r requirements_dev.txt
23+
script:
24+
- python setup.py test
25+
after_success:
26+
- codecov
27+
deploy:
28+
provider: pypi
29+
user: p1c2u
30+
password:
31+
secure: iZWZuDMIWyFtJf5cLDPwA82d7DVi+/8yBQJVowctJwkioz4PEZBrf4N7cKyFc7JlhsS0/gqPJ9nw1FBqHwlTFwikpCYjudcfVijzibwKBbTbYTbTY1xEYiv+2/Q2UGoGjGmf2qdqM9SBaQwvax+KgMO6e4I4vrX4cm3kMx4LHt0Z2ArORlhZ0oKxyi6azcFiZYwlOlp31PuV0iNpBkroBf+gQ20S35hD+GIm1U6D4zqkN0HmZ0LxlpZLXsHZ0FrEE57KU06RowWfkAFBkGjMBjr+phiZ/XRe88SFaiB3HVZaJm+ZPTJKnxryuGt5th54Q10DKLZ3KUien33saBYVziamHZ8ZYS01ztahEhqLKlQVB1e+p1M8mYXKVodqLgytOsddixIBmibq2rDJmLSPwro8RBwLhLdGZdRsH2kii06OQxPrzlUrOwtErozxvdNjS47hwjJ4ZVm4ZGcnOXZut4qwkiEEUMWUd54V+zDNnRxOf+hi/mEx3u8CmkV26XFJ7WHpr/E1T9XHuRh7YVP8MXrM3gjoL86g1swalpH/QBjf0UaF2BlTvWJ3j52uThH7MFUlCBgpYer1giJayyNjFw4+qUVMCyByD87V7x6/3glA7t4Kh0LiMq0Zo23PPbhuJOmJmDy6GTtjkXZEJ6XnNPV9+VR8LApmppevBDKafgA=
32+
distributions: sdist bdist_wheel
33+
on:
34+
tags: true

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include README.md
2+
include requirements.txt
3+
include requirements_dev.txt
4+
include openapi_spec_validator/resources/schemas/v3.0.0/*

README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,46 @@
1-
# openapi-spec-validator
2-
OpenAPI Spec validator
1+
# OpenAPI Spec validator
2+
3+
[![Build Status](https://travis-ci.org/p1c2u/openapi-spec-validator.svg?branch=master)](https://travis-ci.org/p1c2u/openapi-spec-validator)
4+
5+
## About
6+
7+
OpenAPI Spec Validator is a Python library that validates OpenAPI Specs against the [OpenAPI 3.0.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md) specification. The validator aims to check for full compliance with the Specification.
8+
9+
## Installation
10+
11+
$ pip install openapi-spec-validator
12+
13+
## Usage
14+
15+
Validate spec:
16+
17+
```python
18+
19+
from openapi_spec_validator import validate_spec
20+
21+
validate_spec(spec)
22+
```
23+
24+
You can also validate spec from url:
25+
26+
```python
27+
28+
from openapi_spec_validator import validate_spec_url
29+
30+
validate_spec_url('http://example.com/openapi.json')
31+
```
32+
33+
If you want to iterate through validation errors:
34+
35+
```python
36+
37+
from openapi_spec_validator import openapi_v3_validator_factory
38+
39+
validator = openapi_v3_validator_factory.create(spec)
40+
errors_iterator = validator.iter_errors(spec)
41+
```
42+
43+
## License
44+
45+
Copyright (c) 2017, Artur Maciag, All rights reserved.
46+
Apache v2

openapi_spec_validator/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
from openapi_spec_validator.shortcuts import (
3+
validate_spec_factory, validate_spec_url_factory,
4+
)
5+
from openapi_spec_validator.handlers import UrlHandler
6+
from openapi_spec_validator.schemas import get_openapi_schema
7+
from openapi_spec_validator.factories import JSONSpecValidatorFactory
8+
9+
__author__ = 'Artur Maciąg'
10+
__email__ = '[email protected]'
11+
__version__ = '0.0.1'
12+
__url__ = 'https://github.com/p1c2u/openapi_spec_validator'
13+
__license__ = 'Apache License, Version 2.0'
14+
15+
__all__ = ['openapi_v3_validator', 'validate_spec', 'validate_spec_url']
16+
17+
default_handlers = {
18+
'<all_urls>': UrlHandler('http', 'https', 'file'),
19+
'http': UrlHandler('http'),
20+
'https': UrlHandler('https'),
21+
'file': UrlHandler('file'),
22+
}
23+
schema_v3, schema_v3_url = get_openapi_schema('3.0.0')
24+
openapi_v3_validator_factory = JSONSpecValidatorFactory(
25+
schema_v3, schema_v3_url,
26+
resolver_handlers=default_handlers,
27+
)
28+
# shortcuts
29+
validate_spec = validate_spec_factory(
30+
openapi_v3_validator_factory.create)
31+
validate_spec_url = validate_spec_url_factory(
32+
openapi_v3_validator_factory.create, default_handlers)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""OpenAPI spec validator decorators module."""
2+
import logging
3+
4+
from openapi_spec_validator.managers import VisitingManager
5+
6+
log = logging.getLogger(__name__)
7+
8+
9+
class DerefValidatorDecorator:
10+
"""Dereferences instance if it is a $ref before passing it for validation.
11+
12+
:param instance_resolver: Resolves refs in the openapi service spec
13+
"""
14+
def __init__(self, instance_resolver):
15+
self.instance_resolver = instance_resolver
16+
self.visiting = VisitingManager()
17+
18+
def __call__(self, func):
19+
def wrapped(validator, schema_element, instance, schema):
20+
if not isinstance(instance, dict) or '$ref' not in instance:
21+
return func(validator, schema_element, instance, schema)
22+
23+
ref = instance['$ref']
24+
25+
# ref already visited
26+
if ref in self.visiting:
27+
return
28+
29+
self._attach_scope(instance)
30+
with self.visiting.visit(ref):
31+
with self.instance_resolver.resolving(ref) as target:
32+
return func(validator, schema_element, target, schema)
33+
34+
return wrapped
35+
36+
def _attach_scope(self, instance):
37+
log.debug('Attaching x-scope to %s', instance)
38+
if 'x-scope' in instance:
39+
log.debug('Ref %s already has scope attached', instance['$ref'])
40+
return
41+
42+
instance['x-scope'] = list(self.instance_resolver._scopes_stack)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""OpenAPI spec validator factories module."""
2+
from jsonschema import validators
3+
from jsonschema.validators import Draft4Validator, RefResolver
4+
5+
from openapi_spec_validator.generators import (
6+
SpecValidatorsGeneratorFactory,
7+
)
8+
9+
10+
class Draft4ExtendedValidatorFactory(Draft4Validator):
11+
"""Draft4Validator with extra validators factory that follows $refs
12+
in the schema being validated."""
13+
14+
@classmethod
15+
def from_resolver(cls, spec_resolver):
16+
"""Creates a customized Draft4ExtendedValidator.
17+
18+
:param spec_resolver: resolver for the spec
19+
:type resolver: :class:`jsonschema.RefResolver`
20+
"""
21+
spec_validators = cls._get_spec_validators(spec_resolver)
22+
return validators.extend(Draft4Validator, spec_validators)
23+
24+
@classmethod
25+
def _get_spec_validators(cls, spec_resolver):
26+
generator = SpecValidatorsGeneratorFactory.from_spec_resolver(
27+
spec_resolver)
28+
return dict(list(generator))
29+
30+
31+
class JSONSpecValidatorFactory:
32+
"""
33+
Json documents validator factory against a json schema.
34+
35+
:param schema: schema for validation.
36+
:param schema_url: schema base uri.
37+
"""
38+
39+
schema_validator_class = Draft4Validator
40+
spec_validator_factory = Draft4ExtendedValidatorFactory
41+
42+
def __init__(self, schema, schema_url='', resolver_handlers=None):
43+
self.schema = schema
44+
self.schema_url = schema_url
45+
self.resolver_handlers = resolver_handlers or ()
46+
47+
self.schema_validator_class.check_schema(self.schema)
48+
49+
@property
50+
def schema_resolver(self):
51+
return self._get_resolver(self.schema_url, self.schema)
52+
53+
def create(self, spec, spec_url=''):
54+
"""Creates json documents validator from spec.
55+
:param spec: json document in the form of a list or dict.
56+
:param spec_url: base uri to use when creating a
57+
RefResolver for the passed in spec_dict.
58+
59+
:return: RefResolver for spec with cached remote $refs used during
60+
validation.
61+
:rtype: :class:`jsonschema.RefResolver`
62+
"""
63+
spec_resolver = self._get_resolver(spec_url, spec)
64+
65+
validator_cls = self.spec_validator_factory.from_resolver(
66+
spec_resolver)
67+
68+
return validator_cls(
69+
self.schema, resolver=self.schema_resolver)
70+
71+
def _get_resolver(self, base_uri, referrer):
72+
return RefResolver(
73+
base_uri, referrer, handlers=self.resolver_handlers)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""OpenAPI spec validator generators module."""
2+
import logging
3+
4+
from six import iteritems
5+
from jsonschema import _validators
6+
7+
from openapi_spec_validator.decorators import DerefValidatorDecorator
8+
9+
log = logging.getLogger(__name__)
10+
11+
12+
class SpecValidatorsGeneratorFactory:
13+
"""Generator factory for customized validators that follows $refs
14+
in the schema being validated.
15+
"""
16+
17+
validators = {
18+
'$ref': _validators.ref,
19+
'properties': _validators.properties_draft4,
20+
'additionalProperties': _validators.additionalProperties,
21+
'patternProperties': _validators.patternProperties,
22+
'type': _validators.type_draft4,
23+
'dependencies': _validators.dependencies,
24+
'required': _validators.required_draft4,
25+
'minProperties': _validators.minProperties_draft4,
26+
'maxProperties': _validators.maxProperties_draft4,
27+
'allOf': _validators.allOf_draft4,
28+
'oneOf': _validators.oneOf_draft4,
29+
'anyOf': _validators.anyOf_draft4,
30+
'not': _validators.not_draft4,
31+
}
32+
33+
@classmethod
34+
def from_spec_resolver(cls, spec_resolver):
35+
"""Creates validators generator for the spec resolver.
36+
37+
:param spec_resolver: resolver for the spec
38+
:type instance_resolver: :class:`jsonschema.RefResolver`
39+
"""
40+
deref = DerefValidatorDecorator(spec_resolver)
41+
for key, validator_callable in iteritems(cls.validators):
42+
yield key, deref(validator_callable)

openapi_spec_validator/handlers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""OpenAPI spec validator handlers module."""
2+
import contextlib
3+
4+
from six.moves.urllib.parse import urlparse
5+
from six.moves.urllib.request import urlopen
6+
from yaml import safe_load
7+
8+
9+
class UrlHandler:
10+
"""OpenAPI spec validator URL scheme handler."""
11+
12+
def __init__(self, *allowed_schemes):
13+
self.allowed_schemes = allowed_schemes
14+
15+
def __call__(self, url, timeout=1):
16+
assert urlparse(url).scheme in self.allowed_schemes
17+
18+
with contextlib.closing(urlopen(url, timeout=timeout)) as fh:
19+
return safe_load(fh)

openapi_spec_validator/managers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""OpenAPI spec validator managers module."""
2+
from contextlib import contextmanager
3+
4+
5+
class VisitingManager(dict):
6+
"""Visiting manager. Mark keys which being visited."""
7+
8+
@contextmanager
9+
def visit(self, key):
10+
"""Visits key and marks as visited.
11+
Support context manager interface.
12+
13+
:param key: key being visited.
14+
"""
15+
self[key] = key
16+
try:
17+
yield key
18+
finally:
19+
del self[key]

0 commit comments

Comments
 (0)