"""
Prance implements parsers for Swagger/OpenAPI 2.0 and 3.0.0 API specs.
See https://openapis.org/ for details on the specification.
Included is a BaseParser that reads and validates swagger specs, and a
ResolvingParser that additionally resolves any $ref references.
"""
__author__ = "Jens Finkhaeuser"
__copyright__ = "Copyright (c) 2016-2021 Jens Finkhaeuser"
__license__ = "MIT"
__all__ = ("util", "mixins", "cli", "convert")
from packaging.version import Version
try:
from prance._version import version as __version__
except ImportError:
# todo: better gussing
__version__ = "0.20.0+unknown"
# Define our own error class
[docs]class ValidationError(Exception):
pass
from . import mixins
# Placeholder for when no URL is specified for the main spec file
import sys
if sys.platform == "win32": # pragma: nocover
# Placeholder must be absolute
_PLACEHOLDER_URL = "file:///c:/__placeholder_url__.yaml"
else:
_PLACEHOLDER_URL = "file:///__placeholder_url__.yaml"
[docs]class BaseParser(mixins.YAMLMixin, mixins.JSONMixin):
"""
The BaseParser loads, parses and validates OpenAPI 2.0 and 3.0.0 specs.
Uses :py:class:`YAMLMixin` and :py:class:`JSONMixin` for additional
functionality.
"""
BACKENDS = {
"flex": ((2,), "_validate_flex"),
"swagger-spec-validator": ((2,), "_validate_swagger_spec_validator"),
"openapi-spec-validator": ((2, 3), "_validate_openapi_spec_validator"),
}
SPEC_VERSION_2_PREFIX = "Swagger/OpenAPI"
SPEC_VERSION_3_PREFIX = "OpenAPI"
def __init__(self, url=None, spec_string=None, lazy=False, **kwargs):
"""
Load, parse and validate specs.
You can either provide a URL or a spec string, but not both.
:param str url: The URL of the file to load. URLs missing a scheme are
assumed to be file URLs.
:param str spec_string: The specifications to parse.
:param bool lazy: If true, do not load or parse anything. Instead wait for
the parse function to be invoked.
:param str backend: [optional] one of 'flex', 'swagger-spec-validator' or
'openapi-spec-validator'.
Determines the validation backend to use. Defaults to the first installed
backend in the ordered list obtained from util.validation_backends().
:param bool strict: [optional] Applies only to the 'swagger-spec-validator'
backend. If False, accepts non-String keys by stringifying them before
validation. Defaults to True.
:param str encoding: [optional] For local URLs, use the given file encoding
instead of auto-detecting. Defaults to None.
"""
assert url or spec_string and not (url and spec_string), (
"You must provide either a URL to read, or a spec string to "
"parse, but not both!"
)
# Keep the parameters around for later use
self.url = None
if url:
from .util.url import absurl
from .util.fs import abspath
import os
self.url = absurl(url, abspath(os.getcwd()))
else:
self.url = _PLACEHOLDER_URL
self._spec_string = spec_string
# Initialize variables we're filling later
self.specification = None
self.version = None
self.version_name = None
self.version_parsed = ()
self.valid = False
# Add kw args as options
self.options = kwargs
# Verify backend
from .util import default_validation_backend
self.backend = self.options.get("backend", default_validation_backend())
if self.backend not in BaseParser.BACKENDS.keys():
raise ValueError(
f"Backend may only be one of {BaseParser.BACKENDS.keys()}!"
)
# Start parsing if lazy mode is not requested.
if not lazy:
self.parse()
[docs] def parse(self): # noqa: F811
"""
When the BaseParser was lazily created, load and parse now.
You can use this function to re-use an existing parser for parsing
multiple files by setting its url property and then invoking this
function.
"""
strict = self.options.get("strict", True)
# If we have a file name, we need to read that in.
if self.url and self.url != _PLACEHOLDER_URL:
from .util.url import fetch_url
encoding = self.options.get("encoding", None)
self.specification = fetch_url(self.url, encoding=encoding, strict=strict)
# If we have a spec string, try to parse it.
if self._spec_string:
from .util.formats import parse_spec
self.specification = parse_spec(self._spec_string, self.url)
# If we have a parsed spec, convert it to JSON. Then we can validate
# the JSON. At this point, we *require* a parsed specification to exist,
# so we might as well assert.
assert self.specification, "No specification parsed, cannot validate!"
self._validate()
def _validate(self):
# Ensure specification is a mapping
from collections.abc import Mapping
if not isinstance(self.specification, Mapping):
raise ValidationError("Could not parse specifications!")
# Ensure the selected backend supports the given spec version
versions, validator_name = BaseParser.BACKENDS[self.backend]
# Fetch the spec version. Note that this is the spec version the spec
# *claims* to be; we later set the one we actually could validate as.
spec_version = None
if spec_version is None:
spec_version = self.specification.get("openapi", None)
if spec_version is None:
spec_version = self.specification.get("swagger", None)
if spec_version is None:
raise ValidationError(
"Could not determine specification schema " "version!"
)
# Try parsing the spec version, examine the first component.
import packaging.version
parsed = packaging.version.parse(spec_version)
if parsed.major not in versions:
raise ValidationError(
'Version mismatch: selected backend "%s"'
" does not support specified version %s!" % (self.backend, spec_version)
)
# Validate the parsed specs, using the given validation backend.
validator = getattr(self, validator_name)
# Set valid flag according to whether validator succeeds
self.valid = False
validator(parsed)
self.valid = True
def __set_version(self, prefix, version: Version):
self.version_name = prefix
self.version_parsed = version.release
stringified = str(version)
if prefix == BaseParser.SPEC_VERSION_2_PREFIX:
stringified = "%d.%d" % (version.major, version.minor)
self.version = f"{self.version_name} {stringified}"
def _validate_flex(self, spec_version: Version): # pragma: nocover
# Set the version independently of whether validation succeeds
self.__set_version(BaseParser.SPEC_VERSION_2_PREFIX, spec_version)
from flex.exceptions import ValidationError as JSEValidationError
from flex.core import parse as validate
try:
validate(self.specification)
except JSEValidationError as ex:
from .util.exceptions import raise_from
raise_from(ValidationError, ex)
def _validate_swagger_spec_validator(
self, spec_version: Version
): # pragma: nocover
# Set the version independently of whether validation succeeds
self.__set_version(BaseParser.SPEC_VERSION_2_PREFIX, spec_version)
from swagger_spec_validator.common import SwaggerValidationError as SSVErr
from swagger_spec_validator.validator20 import validate_spec
try:
validate_spec(self.specification)
except SSVErr as ex:
from .util.exceptions import raise_from
raise_from(ValidationError, ex)
def _validate_openapi_spec_validator(
self, spec_version: Version
): # pragma: nocover
from openapi_spec_validator import validate_spec
from jsonschema.exceptions import ValidationError as JSEValidationError
from jsonschema.exceptions import RefResolutionError
# Validate according to detected version. Unsupported versions are
# already caught outside of this function.
from .util.exceptions import raise_from
if spec_version.major == 3:
# Set the version independently of whether validation succeeds
self.__set_version(BaseParser.SPEC_VERSION_3_PREFIX, spec_version)
elif spec_version.major == 2:
# Set the version independently of whether validation succeeds
self.__set_version(BaseParser.SPEC_VERSION_2_PREFIX, spec_version)
try:
validate_spec(self.specification)
except TypeError as type_ex: # pragma: nocover
raise_from(ValidationError, type_ex, self._strict_warning())
except JSEValidationError as v2_ex:
raise_from(ValidationError, v2_ex)
except RefResolutionError as ref_ex:
raise_from(ValidationError, ref_ex)
def _strict_warning(self):
"""Return a warning if strict mode is off."""
if self.options.get("strict", True):
return (
"Strict mode enabled (the default), so this could be due to an "
"integer key, such as an HTTP status code."
)
return (
"Strict mode disabled. Prance cannot help you narrow this further "
"down, sorry."
)
[docs]class ResolvingParser(BaseParser):
"""The ResolvingParser extends BaseParser with resolving references by inlining."""
def __init__(self, url=None, spec_string=None, lazy=False, **kwargs):
"""
See :py:class:`BaseParser`.
Resolves JSON pointers/references (i.e. '$ref' keys) before validating the
specs. The implication is that self.specification is fully resolved, and
does not contain any references.
Additional parameters, see :py::class:`util.RefResolver`.
"""
# Create a reference cache
self.__reference_cache = {}
BaseParser.__init__(self, url=url, spec_string=spec_string, lazy=lazy, **kwargs)
def _validate(self):
# We have a problem with the BaseParser's validate function: the
# jsonschema implementation underlying it does not accept relative
# path references, but the Swagger specs allow them:
# http://swagger.io/specification/#referenceObject
# We therefore use our own resolver first, and validate later.
from .util.resolver import RefResolver
forward_arg_names = (
"encoding",
"recursion_limit",
"recursion_limit_handler",
"resolve_types",
"resolve_method",
"strict",
)
forward_args = {
k: v for (k, v) in self.options.items() if k in forward_arg_names
}
resolver = RefResolver(
self.specification,
self.url,
reference_cache=self.__reference_cache,
**forward_args,
)
resolver.resolve_references()
self.specification = resolver.specs
# Now validate - the BaseParser knows the specifics
BaseParser._validate(self)
# Underscored to allow some time for the public API to be stabilized.
class _TranslatingParser(BaseParser):
def _validate(self):
from .util.translator import _RefTranslator
translator = _RefTranslator(self.specification, self.url)
translator.translate_references()
self.specification = translator.specs
BaseParser._validate(self)