class TestRefResolver(TestCase): def setUp(self): self.resolver = RefResolver() self.schema = mock.MagicMock() def test_it_resolves_local_refs(self): ref = "#/properties/foo" resolved = self.resolver.resolve(self.schema, ref) self.assertEqual(resolved, self.schema["properties"]["foo"]) def test_it_retrieves_non_local_refs(self): schema = '{"type" : "integer"}' get_page = mock.Mock(return_value=StringIO(schema)) resolver = RefResolver(get_page=get_page) url = "http://example.com/schema" resolved = resolver.resolve(mock.Mock(), url) self.assertEqual(resolved, json.loads(schema)) get_page.assert_called_once_with(url) def test_it_uses_urlopen_by_default_for_nonlocal_refs(self): self.assertEqual(self.resolver.get_page, urlopen) def test_it_accepts_a_ref_store(self): store = mock.Mock() self.assertEqual(RefResolver(store).store, store) def test_it_retrieves_stored_refs(self): ref = self.resolver.store["cached_ref"] = mock.Mock() resolved = self.resolver.resolve(self.schema, "cached_ref") self.assertEqual(resolved, ref)
class TestRefResolver(TestCase): def setUp(self): self.resolver = RefResolver() self.schema = mock.MagicMock() def test_it_resolves_local_refs(self): ref = "#/properties/foo" resolved = self.resolver.resolve(self.schema, ref) self.assertEqual(resolved, self.schema["properties"]["foo"]) def test_it_retrieves_non_local_refs(self): schema = '{"type" : "integer"}' get_page = mock.Mock(return_value=StringIO(schema)) resolver = RefResolver(get_page=get_page) url = "http://example.com/schema" resolved = resolver.resolve(mock.Mock(), url) self.assertEqual(resolved, json.loads(schema)) get_page.assert_called_once_with(url) def test_it_uses_urlopen_by_default_for_nonlocal_refs(self): self.assertEqual(self.resolver.get_page, urlopen) def test_it_accepts_a_ref_store(self): store = mock.Mock() self.assertEqual(RefResolver(store).store, store) def test_it_retrieves_stored_refs(self): ref = self.resolver.store["cached_ref"] = mock.Mock() resolved = self.resolver.resolve(self.schema, "cached_ref") self.assertEqual(resolved, ref)
class TestRefResolver(unittest.TestCase): def setUp(self): self.base_uri = "" self.referrer = {} self.store = {} self.resolver = RefResolver(self.base_uri, self.referrer, self.store) def test_it_does_not_retrieve_schema_urls_from_the_network(self): ref = Draft3Validator.META_SCHEMA["id"] with mock.patch.object(self.resolver, "resolve_remote") as remote: resolved = self.resolver.resolve(ref) self.assertEqual(resolved, Draft3Validator.META_SCHEMA) self.assertFalse(remote.called) def test_it_resolves_local_refs(self): ref = "#/properties/foo" self.referrer["properties"] = {"foo" : object()} resolved = self.resolver.resolve(ref) self.assertEqual(resolved, self.referrer["properties"]["foo"]) def test_it_retrieves_stored_refs(self): self.resolver.store["cached_ref"] = {"foo" : 12} resolved = self.resolver.resolve("cached_ref#/foo") self.assertEqual(resolved, 12) def test_it_retrieves_unstored_refs_via_urlopen(self): ref = "http://bar#baz" schema = {"baz" : 12} with mock.patch("jsonschema.urlopen") as urlopen: urlopen.return_value.read.return_value = json.dumps(schema) resolved = self.resolver.resolve(ref) self.assertEqual(resolved, 12) urlopen.assert_called_once_with("http://bar") def test_it_can_construct_a_base_uri_from_a_schema(self): schema = {"id" : "foo"} resolver = RefResolver.from_schema(schema) self.assertEqual(resolver.base_uri, "foo") self.assertEqual(resolver.referrer, schema) def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): schema = {} resolver = RefResolver.from_schema(schema) self.assertEqual(resolver.base_uri, "") self.assertEqual(resolver.referrer, schema)
def resolve_reference(self, schema): schema = deepcopy(schema) # avoid changing the original schema ref = schema['schema'] ref_resolver = RefResolver('', None) d = ref_resolver.resolve(ref['$ref']) schema['schema'] = d return schema
def _resolve_schema_references(self, schema: dict, resolver: RefResolver) -> dict: if "$ref" in schema: reference_path = schema.pop("$ref", None) resolved = resolver.resolve(reference_path)[1] schema.update(resolved) return self._resolve_schema_references(schema, resolver) if "properties" in schema: for k, val in schema["properties"].items(): schema["properties"][k] = self._resolve_schema_references( val, resolver) if "patternProperties" in schema: for k, val in schema["patternProperties"].items(): schema["patternProperties"][ k] = self._resolve_schema_references(val, resolver) if "items" in schema: schema["items"] = self._resolve_schema_references( schema["items"], resolver) if "anyOf" in schema: for i, element in enumerate(schema["anyOf"]): schema["anyOf"][i] = self._resolve_schema_references( element, resolver) return schema
def get_security_definitions(self, schema: Dict[str, Any], resolver: RefResolver) -> Dict[str, Any]: """In Open API 3 security definitions are located in ``components`` and may have references inside.""" components = schema.get("components", {}) security_schemes = components.get("securitySchemes", {}) if "$ref" in security_schemes: return resolver.resolve(security_schemes["$ref"])[1] return security_schemes
def _deep_resolve_mapping( unresolved: JSONMapping, resolver: jsonschema.RefResolver ) -> JSONMapping: return { k: deep_resolve(resolver.resolve(v["$ref"])[-1] if "$ref" in v else v, resolver) for k, v in unresolved.items() }
def test_it_retrieves_non_local_refs(self): schema = '{"type" : "integer"}' get_page = mock.Mock(return_value=StringIO(schema)) resolver = RefResolver(get_page=get_page) url = "http://example.com/schema" resolved = resolver.resolve(mock.Mock(), url) self.assertEqual(resolved, json.loads(schema)) get_page.assert_called_once_with(url)
def test_it_retrieves_non_local_refs(self): schema = '{"type" : "integer"}' get_page = mock.Mock(return_value=StringIO(schema)) resolver = RefResolver(get_page=get_page) url = "http://example.com/schema" resolved = resolver.resolve(mock.Mock(), url) self.assertEqual(resolved, json.loads(schema)) get_page.assert_called_once_with(url)
def _deep_resolve_sequence( unresolved: Sequence, resolver: jsonschema.RefResolver ) -> Sequence: return unresolved.__class__( # type: ignore [ deep_resolve( resolver.resolve(item["$ref"])[-1] if "$ref" in item else item, resolver ) for item in unresolved ] )
def _resolve_reference(value, root, resolvers): base, ref = value.split("#", 1) if base: resolver, new_root = resolvers[base] referrer, resolution = resolver.resolve(value) _resolve_schema(resolution, new_root, resolvers) else: resolver = RefResolver("#", root) referrer, resolution = resolver.resolve(value) return resolution
def resolve_reference(self, value, root): """Resolves a reference. :param value: The actual reference, e.g. ``_yaml.yaml#/def`` :param root: The containing root of :param:`value`. This needs to be passed in order to resolve self-referential $refs, e.g. ``#/def``. :returns: JSON Schema pointed to by :param:`value` """ base, ref = value.split('#', 1) if base: resolver, new_root = self.resolvers[base] referrer, resolution = resolver.resolve(value) self.resolve_schema(resolution, new_root) else: resolver = RefResolver('#', root) referrer, resolution = resolver.resolve(value) return resolution
class JsonSchemaResolver(SchemaResolverInterface): """ Object to resolve schemas using `jsonschema` as references fetcher The implementation is based on the one provided by Giordon Stark: https://gist.github.com/kratsg/96cec81df8c0d78ebdf14bf7b800e938 """ def __init__(self, schemas_uri): """ Initializes the inner $ref `jsonschema` resolver. :param schemas_uri: str. """ if not schemas_uri.endswith("/"): schemas_uri += "/" self.schemas_uri = schemas_uri self.ref_fetcher = RefResolver(base_uri=self.schemas_uri, referrer=None) def _walk_dict(self, obj, ref): """ Iterates a dictionary within the schema resolving every $ref. :param obj: dict. :param ref: str. :return: dict. """ out_obj = deepcopy(obj) for key in obj: if key == '$ref': path = urljoin(ref, out_obj.pop(key)) new_ref, new_obj = self.ref_fetcher.resolve(path) resolved_obj = self._walk_dict(new_obj, new_ref) out_obj.update(resolved_obj) elif isinstance(obj[key], dict): out_obj[key] = self._walk_dict(obj[key], ref) elif isinstance(obj[key], list): out_obj[key] = self._walk_list(obj[key], ref) return out_obj def _walk_list(self, seq, ref): """ Iterates a sequence within the schema resolving every $ref. :param seq: list. :param ref: str. :return: list. """ items = [] for item in seq: if isinstance(item, dict): item = self._walk_dict(item, ref) items.append(item) else: items.append(item) return items def resolve(self, schema_uri): """ Resolves a JSON schema either from a remote or a local URI. :param schema_uri: str. :return: dict. """ top_ref, top_obj = self.ref_fetcher.resolve(schema_uri) resolved_schema = self._walk_dict(top_obj, top_ref) return resolved_schema
class APIDefinition: version = None path_class = Path operation_class = None def __init__(self, doc): """ Instantiate a new Lepo router. :param doc: The OpenAPI definition document. :type doc: dict """ self.doc = doc self.resolver = RefResolver('', self.doc) def resolve_reference(self, ref): """ Resolve a JSON Pointer object reference to the object itself. :param ref: Reference string (`#/foo/bar`, for instance) :return: The object, if found :raises jsonschema.exceptions.RefResolutionError: if there is trouble resolving the reference """ url, resolved = self.resolver.resolve(ref) return resolved def get_path_mapping(self, path): return maybe_resolve(self.doc['paths'][path], self.resolve_reference) def get_path_names(self): yield from self.doc['paths'] def get_path(self, path): """ Construct a Path object from a path string. The Path string must be declared in the API. :type path: str :rtype: lepo.path.Path """ mapping = self.get_path_mapping(path) return self.path_class(api=self, path=path, mapping=mapping) def get_paths(self): """ Iterate over all Path objects declared by the API. :rtype: Iterable[lepo.path.Path] """ for path_name in self.get_path_names(): yield self.get_path(path_name) @classmethod def from_file(cls, filename): """ Construct an APIDefinition by parsing the given `filename`. If PyYAML is installed, YAML files are supported. JSON files are always supported. :param filename: The filename to read. :rtype: APIDefinition """ with open(filename) as infp: if filename.endswith('.yaml') or filename.endswith('.yml'): import yaml data = yaml.safe_load(infp) else: import json data = json.load(infp) return cls.from_data(data) @classmethod def from_data(cls, data): version = parse_version(data) if version == SWAGGER_2: return Swagger2APIDefinition(data) if version == OPENAPI_3: return OpenAPI3APIDefinition(data) raise NotImplementedError('We can never get here.') # pragma: no cover @classmethod def from_yaml(cls, yaml_string): from yaml import safe_load return cls.from_data(safe_load(yaml_string))
class Router: path_class = Path def __init__(self, api): """ Instantiate a new Lepo router. :param api: The OpenAPI definition object. :type api: dict """ self.api = deepcopy(api) self.api.pop('host', None) self.handlers = {} self.resolver = RefResolver('', self.api) @classmethod def from_file(cls, filename): """ Construct a Router by parsing the given `filename`. If PyYAML is installed, YAML files are supported. JSON files are always supported. :param filename: The filename to read. :rtype: Router """ with open(filename) as infp: if filename.endswith('.yaml') or filename.endswith('.yml'): import yaml data = yaml.safe_load(infp) else: import json data = json.load(infp) return cls(data) def get_path(self, path): """ Construct a Path object from a path string. The Path string must be declared in the API. :type path: str :rtype: lepo.path.Path """ mapping = maybe_resolve(self.api['paths'][path], self.resolve_reference) return self.path_class(router=self, path=path, mapping=mapping) def get_paths(self): """ Iterate over all Path objects declared by the API. :rtype: Iterable[lepo.path.Path] """ for path in self.api['paths']: yield self.get_path(path) def get_urls( self, root_view_name=None, optional_trailing_slash=False, decorate=(), name_template='{name}', ): """ Get the router's URLs, ready to be installed in `urlpatterns` (directly or via `include`). :param root_view_name: The optional url name for an API root view. This may be useful for projects that do not explicitly know where the router is mounted; those projects can then use `reverse('api:root')`, for instance, if they need to construct URLs based on the API's root URL. :type root_view_name: str|None :param optional_trailing_slash: Whether to fix up the regexen for the router to make any trailing slashes optional. :type optional_trailing_slash: bool :param decorate: A function to decorate view functions with, or an iterable of such decorators. Use `(lepo.decorators.csrf_exempt,)` to mark all API views as CSRF exempt. :type decorate: function|Iterable[function] :param name_template: A `.format()` template for view naming. :type name_template: str :return: List of URL tuples. :rtype: list[tuple] """ if isinstance(decorate, Iterable): decorators = decorate def decorate(view): return reduce(lambda view, decorator: decorator(view), decorators, view) urls = [] for path in self.get_paths(): regex = path.regex if optional_trailing_slash: regex = regex.rstrip('$') if not regex.endswith('/'): regex += '/' regex += '?$' view = decorate(path.view_class.as_view()) urls.append( url(regex, view, name=name_template.format(name=path.name))) if root_view_name: urls.append( url(r'^$', root_view, name=name_template.format(name=root_view_name))) return urls def get_handler(self, operation_id): """ Get the handler function for a given operation. To remain Pythonic, both the original and the snake_cased version of the operation ID are supported. This could be overridden in a subclass. :param operation_id: Operation ID. :return: Handler function :rtype: function """ handler = (self.handlers.get(operation_id) or self.handlers.get(snake_case(operation_id))) if handler: return handler raise MissingHandler( 'Missing handler for operation %s (tried %s too)' % (operation_id, snake_case(operation_id))) def add_handlers(self, namespace): """ Add handler functions from the given `namespace`, for instance a module. The namespace may be a string, in which case it is expected to be a name of a module. It may also be a dictionary mapping names to functions. Only non-underscore-prefixed functions and methods are imported. :param namespace: Namespace object. :type namespace: str|module|dict[str, function] """ if isinstance(namespace, str): namespace = import_module(namespace) if isinstance(namespace, dict): namespace = namespace.items() else: namespace = vars(namespace).items() for name, value in namespace: if name.startswith('_'): continue if isfunction(value) or ismethod(value): self.handlers[name] = value def resolve_reference(self, ref): """ Resolve a JSON Pointer object reference to the object itself. :param ref: Reference string (`#/foo/bar`, for instance) :return: The object, if found :raises jsonschema.exceptions.RefResolutionError: if there is trouble resolving the reference """ url, resolved = self.resolver.resolve(ref) return resolved