Source code for invenio_jsonschemas.ext

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015-2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Invenio module for building and serving JSONSchemas."""

from __future__ import absolute_import, print_function

import json
import os

import pkg_resources
import six
from flask import request
from jsonref import JsonRef
from six.moves.urllib.parse import urlsplit
from werkzeug.exceptions import HTTPException
from werkzeug.routing import Map, Rule
from werkzeug.utils import cached_property, import_string

from . import config
from .errors import JSONSchemaDuplicate, JSONSchemaNotFound
from .views import create_blueprint

try:
    from functools import lru_cache
except ImportError:
    from functools32 import lru_cache


[docs]class InvenioJSONSchemasState(object): """InvenioJSONSchemas state and api.""" def __init__(self, app): """Constructor. :param app: application registering this state """ self.app = app self.schemas = {} self.url_map = Map([Rule( '{0}/<path:path>'.format(self.app.config['JSONSCHEMAS_ENDPOINT']), endpoint='schema', host=self.app.config['JSONSCHEMAS_HOST'], )], host_matching=True)
[docs] def register_schemas_dir(self, directory): """Recursively register all json-schemas in a directory. :param directory: directory path. """ for root, dirs, files in os.walk(directory): dir_path = os.path.relpath(root, directory) if dir_path == '.': dir_path = '' for file_ in files: if file_.lower().endswith(('.json')): schema_name = os.path.join(dir_path, file_) if schema_name in self.schemas: raise JSONSchemaDuplicate( schema_name, self.schemas[schema_name], directory ) self.schemas[schema_name] = os.path.abspath(directory)
[docs] def register_schema(self, directory, path): """Register a json-schema. :param directory: root directory path. :param path: schema path, relative to the root directory. """ self.schemas[path] = os.path.abspath(directory)
[docs] def get_schema_dir(self, path): """Retrieve the directory containing the given schema. :param path: Schema path, relative to the directory where it was registered. :raises invenio_jsonschemas.errors.JSONSchemaNotFound: If no schema was found in the specified path. :returns: The schema directory. """ if path not in self.schemas: raise JSONSchemaNotFound(path) return self.schemas[path]
[docs] def get_schema_path(self, path): """Compute the schema's absolute path from a schema relative path. :param path: relative path of the schema. :raises invenio_jsonschemas.errors.JSONSchemaNotFound: If no schema was found in the specified path. :returns: The absolute path. """ if path not in self.schemas: raise JSONSchemaNotFound(path) return os.path.join(self.schemas[path], path)
[docs] @lru_cache(maxsize=1000) def get_schema(self, path, with_refs=False, resolved=False): """Retrieve a schema. :param path: schema's relative path. :param with_refs: replace $refs in the schema. :param resolved: resolve schema using the resolver :py:const:`invenio_jsonschemas.config.JSONSCHEMAS_RESOLVER_CLS` :raises invenio_jsonschemas.errors.JSONSchemaNotFound: If no schema was found in the specified path. :returns: The schema in a dictionary form. """ if path not in self.schemas: raise JSONSchemaNotFound(path) with open(os.path.join(self.schemas[path], path)) as file_: schema = json.load(file_) if with_refs: schema = JsonRef.replace_refs( schema, base_uri=request.base_url, loader=self.loader_cls() if self.loader_cls else None, ) if resolved: schema = self.resolver_cls(schema) return schema
[docs] def list_schemas(self): """List all JSON-schema names. :returns: list of schema names. :rtype: list """ return self.schemas.keys()
[docs] def url_to_path(self, url): """Convert schema URL to path. :param url: The schema URL. :returns: The schema path or ``None`` if the schema can't be resolved. """ parts = urlsplit(url) try: loader, args = self.url_map.bind(parts.netloc).match(parts.path) path = args.get('path') if loader == 'schema' and path in self.schemas: return path except HTTPException: return None
[docs] def path_to_url(self, path): """Build URL from a path. :param path: relative path of the schema. :returns: The schema complete URL or ``None`` if not found. """ if path not in self.schemas: return None return self.url_map.bind( self.app.config['JSONSCHEMAS_HOST'], url_scheme=self.app.config['JSONSCHEMAS_URL_SCHEME'] ).build( 'schema', values={'path': path}, force_external=True)
[docs] @cached_property def loader_cls(self): """Loader class used in `JsonRef.replace_refs`.""" cls = self.app.config['JSONSCHEMAS_LOADER_CLS'] if isinstance(cls, six.string_types): return import_string(cls) return cls
[docs] @cached_property def resolver_cls(self): """Loader to resolve the schema.""" cls = self.app.config['JSONSCHEMAS_RESOLVER_CLS'] if isinstance(cls, six.string_types): return import_string(cls) return cls
[docs]class InvenioJSONSchemas(object): """Invenio-JSONSchemas extension. Register blueprint serving registered schemas and can be used as an api to register those schemas. .. note:: JSON schemas are served as static files. Thus their "id" and "$ref" fields might not match the Flask application's host and port. """ def __init__(self, app=None, **kwargs): """Extension initialization. :param app: The Flask application. (Default: ``None``) """ self.kwargs = kwargs if app: self.init_app(app, **kwargs)
[docs] def init_app(self, app, entry_point_group=None, register_blueprint=True, register_config_blueprint=None): """Flask application initialization. :param app: The Flask application. :param entry_point_group: The group entry point to load extensions. (Default: ``invenio_jsonschemas.schemas``) :param register_blueprint: Register the blueprints. :param register_config_blueprint: Register blueprint for the specific app from a config variable. """ self.init_config(app) if not entry_point_group: entry_point_group = self.kwargs['entry_point_group'] \ if 'entry_point_group' in self.kwargs \ else 'invenio_jsonschemas.schemas' state = InvenioJSONSchemasState(app) # Load the json-schemas from extension points. if entry_point_group: for base_entry in pkg_resources.iter_entry_points( entry_point_group): directory = os.path.dirname(base_entry.load().__file__) state.register_schemas_dir(directory) # Init blueprints _register_blueprint = app.config.get(register_config_blueprint) if _register_blueprint is not None: register_blueprint = _register_blueprint if register_blueprint: app.register_blueprint( create_blueprint(state), url_prefix=app.config['JSONSCHEMAS_ENDPOINT'] ) self._state = app.extensions['invenio-jsonschemas'] = state return state
[docs] def init_config(self, app): """Initialize configuration.""" for k in dir(config): if k.startswith('JSONSCHEMAS_'): app.config.setdefault(k, getattr(config, k)) host_setting = app.config['JSONSCHEMAS_HOST'] if not host_setting or host_setting == 'localhost': app.logger.warning('JSONSCHEMAS_HOST is set to {0}'.format( host_setting))
def __getattr__(self, name): """Proxy to state object.""" return getattr(self._state, name, None)
[docs]class InvenioJSONSchemasUI(InvenioJSONSchemas): """Invenio-JSONSchemas extension for UI."""
[docs] def init_app(self, app): """Flask application initialization. :param app: The Flask application. """ return super(InvenioJSONSchemasUI, self).init_app( app, register_config_blueprint='JSONSCHEMAS_REGISTER_ENDPOINTS_UI' )
[docs]class InvenioJSONSchemasAPI(InvenioJSONSchemas): """Invenio-JSONSchemas extension for API."""
[docs] def init_app(self, app): """Flask application initialization. :param app: The Flask application. """ return super(InvenioJSONSchemasAPI, self).init_app( app, register_config_blueprint='JSONSCHEMAS_REGISTER_ENDPOINTS_API' )