Exemplo n.º 1
0
class ApiSpecPlugin(BasePlugin, DocBlueprintMixin):
    """Плагин для связки json_api и swagger"""
    def __init__(self,
                 app=None,
                 spec_kwargs=None,
                 decorators=None,
                 tags: Dict[str, str] = None):
        """

        :param spec_kwargs:
        :param decorators:
        :param tags: {'<name tag>': '<description tag>'}
        """
        self.decorators_for_autodoc = decorators or tuple()
        self.spec_kwargs = spec_kwargs if spec_kwargs is not None else {}
        self.spec = None
        self.spec_tag = {}
        self.spec_schemas = {}
        self.app = None
        self._fields = []
        # Use lists to enforce order
        self._fields = []
        self._converters = []

        # Инициализация ApiSpec
        self.app = app
        self._app = app
        # Initialize spec
        openapi_version = app.config.get("OPENAPI_VERSION", "2.0")
        openapi_major_version = int(openapi_version.split(".")[0])
        if openapi_major_version < 3:
            base_path = app.config.get("APPLICATION_ROOT")
            # Don't pass basePath if '/' to avoid a bug in apispec
            # https://github.com/marshmallow-code/apispec/issues/78#issuecomment-431854606
            # TODO: Remove this condition when the bug is fixed
            if base_path != "/":
                self.spec_kwargs.setdefault("basePath", base_path)
        self.spec_kwargs.update(app.config.get("API_SPEC_OPTIONS", {}))
        self.spec = APISpec(
            app.name,
            app.config.get("API_VERSION", "1"),
            openapi_version=openapi_version,
            plugins=[MarshmallowPlugin(), RestfulPlugin()],
            **self.spec_kwargs,
        )

        tags = tags if tags else {}
        for tag_name, tag_description in tags.items():
            self.spec_tag[tag_name] = {
                "name": tag_name,
                "description": tag_description,
                "add_in_spec": False
            }
            self._add_tags_in_spec(self.spec_tag[tag_name])

    def after_init_plugin(self, *args, app=None, **kwargs):
        # Register custom fields in spec
        for args in self._fields:
            self.spec.register_field(*args)
        # Register custom converters in spec
        for args in self._converters:
            self.spec.register_converter(*args)

        # Initialize blueprint serving spec
        self._register_doc_blueprint()

    def after_route(
        self,
        resource: Union[ResourceList, ResourceDetail] = None,
        view=None,
        urls: Tuple[str] = None,
        self_json_api: Api = None,
        tag: str = None,
        default_parameters=None,
        default_schema: Schema = None,
        **kwargs,
    ) -> None:
        """

        :param resource:
        :param view:
        :param urls:
        :param self_json_api:
        :param str tag: тег под которым стоит связать этот ресурс
        :param default_parameters: дефолтные поля для ресурса в сваггер (иначе просто инициализируется [])
        :param Schema default_schema: схема, которая подставиться вместо схемы в стили json api
        :param kwargs:
        :return:
        """
        # Register views in API documentation for this resource
        # resource.register_views_in_doc(self._app, self.spec)
        # Add tag relative to this resource to the global tag list

        # We add definitions (models) to the apiscpec
        if resource.schema:
            self._add_definitions_in_spec(resource.schema)

        # We add tags to the apiscpec
        tag_name = view.title()
        if tag is None and view.title() not in self.spec_tag:
            dict_tag = {
                "name": view.title(),
                "description": "",
                "add_in_spec": False
            }
            self.spec_tag[dict_tag["name"]] = dict_tag
            self._add_tags_in_spec(dict_tag)
        elif tag:
            tag_name = self.spec_tag[tag]["name"]

        urls = urls if urls else tuple()
        for i_url in urls:
            self._add_paths_in_spec(
                path=i_url,
                resource=resource,
                default_parameters=default_parameters,
                default_schema=default_schema,
                tag_name=tag_name,
                **kwargs,
            )

    @property
    def param_id(self) -> dict:
        return {
            "in": "path",
            "name": "id",
            "required": True,
            "type": "integer",
            "format": "int32",
        }

    @classmethod
    def _get_operations_for_all(cls, tag_name: str,
                                default_parameters: list) -> Dict[str, Any]:
        """
        Creating base dict

        :param tag_name:
        :param default_parameters:
        :return:
        """
        return {
            "tags": [tag_name],
            "produces": ["application/json"],
            "parameters": default_parameters if default_parameters else [],
        }

    @classmethod
    def __get_parameters_for_include_models(cls, resource: Resource) -> dict:
        fields_names = [
            i_field_name for i_field_name, i_field in
            resource.schema._declared_fields.items()
            if isinstance(i_field, Relationship)
        ]
        models_for_include = ",".join(fields_names)
        example_models_for_include = "\n".join(
            [f"`{f}`" for f in fields_names])
        return {
            "default":
            models_for_include,
            "name":
            "include",
            "in":
            "query",
            "format":
            "string",
            "required":
            False,
            "description":
            f"Related relationships to include.\nAvailable:\n{example_models_for_include}",
        }

    @classmethod
    def __get_parameters_for_sparse_fieldsets(cls, resource: Resource,
                                              description: str) -> dict:
        # Sparse Fieldsets
        return {
            "name": f"fields[{resource.schema.Meta.type_}]",
            "in": "query",
            "type": "array",
            "required": False,
            "description": description.format(resource.schema.Meta.type_),
            "items": {
                "type": "string",
                "enum": list(resource.schema._declared_fields.keys())
            },
        }

    def __get_parameters_for_declared_fields(
            self, resource, description) -> Generator[dict, None, None]:
        type_schemas = {resource.schema.Meta.type_}
        for i_field_name, i_field in resource.schema._declared_fields.items():
            if not (isinstance(i_field, Relationship)
                    and i_field.schema.Meta.type_ not in type_schemas):
                continue
            schema_name = create_schema_name(schema=i_field.schema)
            new_parameter = {
                "name": f"fields[{i_field.schema.Meta.type_}]",
                "in": "query",
                "type": "array",
                "required": False,
                "description": description.format(i_field.schema.Meta.type_),
                "items": {
                    "type":
                    "string",
                    "enum":
                    list(self.spec.components.schemas[schema_name]
                         ["properties"].keys()),
                },
            }
            type_schemas.add(i_field.schema.Meta.type_)
            yield new_parameter

    @property
    def __list_filters_data(self) -> tuple:
        return (
            {
                "default": 1,
                "name": "page[number]",
                "in": "query",
                "format": "int64",
                "required": False,
                "description": "Page offset",
            },
            {
                "default": 10,
                "name": "page[size]",
                "in": "query",
                "format": "int64",
                "required": False,
                "description": "Max number of items",
            },
            {
                "name": "sort",
                "in": "query",
                "format": "string",
                "required": False,
                "description": "Sort",
            },
            {
                "name":
                "filter",
                "in":
                "query",
                "format":
                "string",
                "required":
                False,
                "description":
                "Filter (https://flask-combo-jsonapi.readthedocs.io/en/latest/filtering.html)",
            },
        )

    @classmethod
    def _update_parameter_for_field_spec(cls, new_param: dict,
                                         fld_sped: dict) -> None:
        """
        :param new_param:
        :param fld_sped:
        :return:
        """
        if "items" in fld_sped:
            new_items = {
                "type": fld_sped["items"].get("type"),
            }
            if "enum" in fld_sped["items"]:
                new_items["enum"] = fld_sped["items"]["enum"]
            new_param.update({"items": new_items})

    def __get_parameter_for_not_nested(self, field_name, field_spec) -> dict:
        new_parameter = {
            "name": f"filter[{field_name}]",
            "in": "query",
            "type": field_spec.get("type"),
            "required": False,
            "description": f"{field_name} attribute filter",
        }
        self._update_parameter_for_field_spec(new_parameter, field_spec)
        return new_parameter

    def __get_parameter_for_nested_with_filtering(self, field_name,
                                                  field_jsonb_name,
                                                  field_jsonb_spec):
        new_parameter = {
            "name":
            f"filter[{field_name}{SPLIT_REL}{field_jsonb_name}]",
            "in":
            "query",
            "type":
            field_jsonb_spec.get("type"),
            "required":
            False,
            "description":
            f"{field_name}{SPLIT_REL}{field_jsonb_name} attribute filter",
        }
        self._update_parameter_for_field_spec(new_parameter, field_jsonb_spec)
        return new_parameter

    def __get_parameters_for_nested_with_filtering(
            self, field, field_name) -> Generator[dict, None, None]:
        # Allow JSONB filtering
        field_schema_name = create_schema_name(schema=field.schema)
        component_schema = self.spec.components.schemas[field_schema_name]
        for i_field_jsonb_name, i_field_jsonb in field.schema._declared_fields.items(
        ):
            i_field_jsonb_spec = component_schema["properties"][
                i_field_jsonb_name]
            if i_field_jsonb_spec.get("type") == "object":
                # Пропускаем создание фильтров для dict. Просто не понятно как фильтровать по таким
                # полям
                continue
            new_parameter = self.__get_parameter_for_nested_with_filtering(
                field_name,
                i_field_jsonb_name,
                i_field_jsonb_spec,
            )
            yield new_parameter

    def __get_list_resource_fields_filters(
            self, resource) -> Generator[dict, None, None]:
        schema_name = create_schema_name(schema=resource.schema)
        for i_field_name, i_field in resource.schema._declared_fields.items():
            i_field_spec = self.spec.components.schemas[schema_name][
                "properties"][i_field_name]
            if not isinstance(i_field, fields.Nested):
                if i_field_spec.get("type") == "object":
                    # Skip filtering by dicts
                    continue
                yield self.__get_parameter_for_not_nested(
                    i_field_name, i_field_spec)
            elif getattr(i_field.schema.Meta, "filtering", False):
                yield from self.__get_parameters_for_nested_with_filtering(
                    i_field, i_field_name)

    def _get_operations_for_get(self, resource, tag_name, default_parameters):
        operations_get = self._get_operations_for_all(tag_name,
                                                      default_parameters)
        operations_get["responses"] = {
            **status[HTTPStatus.OK],
            **status[HTTPStatus.NOT_FOUND],
        }

        if issubclass(resource, ResourceDetail):
            operations_get["parameters"].append(self.param_id)

        if resource.schema is None:
            return operations_get

        description = "List that refers to the name(s) of the fields to be returned `{}`"

        operations_get["parameters"].extend((
            self.__get_parameters_for_include_models(resource),
            self.__get_parameters_for_sparse_fieldsets(resource, description),
        ))
        operations_get["parameters"].extend(
            self.__get_parameters_for_declared_fields(resource, description))

        if issubclass(resource, ResourceList):
            operations_get["parameters"].extend(self.__list_filters_data)
            operations_get["parameters"].extend(
                self.__get_list_resource_fields_filters(resource))

        return operations_get

    def _get_operations_for_post(self, schema: dict, tag_name: str,
                                 default_parameters: list) -> dict:
        operations = self._get_operations_for_all(tag_name, default_parameters)
        operations["responses"] = {
            "201": {
                "description": "Created"
            },
            "202": {
                "description": "Accepted"
            },
            "403": {
                "description":
                "This implementation does not accept client-generated IDs"
            },
            "404": {
                "description": "Not Found"
            },
            "409": {
                "description": "Conflict"
            },
        }
        operations["parameters"].append({
            "name":
            "POST body",
            "in":
            "body",
            "schema":
            schema,
            "required":
            True,
            "description":
            f"{tag_name} attributes",
        })
        return operations

    def _get_operations_for_patch(self, schema: dict, tag_name: str,
                                  default_parameters: list) -> dict:
        operations = self._get_operations_for_all(tag_name, default_parameters)
        operations["responses"] = {
            "200": {
                "description": "Success"
            },
            "201": {
                "description": "Created"
            },
            "204": {
                "description": "No Content"
            },
            "403": {
                "description": "Forbidden"
            },
            "404": {
                "description": "Not Found"
            },
            "409": {
                "description": "Conflict"
            },
        }
        operations["parameters"].append(self.param_id)
        operations["parameters"].append({
            "name":
            "POST body",
            "in":
            "body",
            "schema":
            schema,
            "required":
            True,
            "description":
            f"{tag_name} attributes",
        })
        return operations

    def _get_operations_for_delete(self, tag_name: str,
                                   default_parameters: list) -> dict:
        operations = self._get_operations_for_all(tag_name, default_parameters)
        operations["parameters"].append(self.param_id)
        operations["responses"] = {
            "200": {
                "description": "Success"
            },
            "202": {
                "description": "Accepted"
            },
            "204": {
                "description": "No Content"
            },
            "403": {
                "description": "Forbidden"
            },
            "404": {
                "description": "Not Found"
            },
        }
        return operations

    def _add_paths_in_spec(
        self,
        path: str = "",
        resource: Any = None,
        tag_name: str = "",
        default_parameters: List = None,
        default_schema: Schema = None,
        **kwargs,
    ) -> None:
        operations = {}
        methods: Set[str] = {i_method.lower() for i_method in resource.methods}

        attributes = {}
        if resource.schema:
            attributes = self.spec.get_ref("schema",
                                           create_schema_name(resource.schema))
        schema = (default_schema if default_schema else {
            "type": "object",
            "properties": {
                "data": {
                    "type": "object",
                    "properties": {
                        "type": {
                            "type": "string",
                        },
                        "id": {
                            "type": "string",
                        },
                        "attributes": attributes,
                        "relationships": {
                            "type": "object",
                        },
                    },
                    "required": [
                        "type",
                    ],
                },
            },
        })

        if "get" in methods:
            operations["get"] = self._get_operations_for_get(
                resource, tag_name, default_parameters)
        if "post" in methods:
            operations["post"] = self._get_operations_for_post(
                schema, tag_name, default_parameters)
        if "patch" in methods:
            operations["patch"] = self._get_operations_for_patch(
                schema, tag_name, default_parameters)
        if "delete" in methods:
            operations["delete"] = self._get_operations_for_delete(
                tag_name, default_parameters)
        rule = None
        for i_rule in self.app.url_map._rules:
            if i_rule.rule == path:
                rule = i_rule
                break
        if APISPEC_VERSION_MAJOR < 1:
            self.spec.add_path(path=path,
                               operations=operations,
                               rule=rule,
                               resource=resource,
                               **kwargs)
        else:
            self.spec.path(path=path,
                           operations=operations,
                           rule=rule,
                           resource=resource,
                           **kwargs)

    def _add_definitions_in_spec(self, schema) -> None:
        """
        Add schema in spec
        :param schema: schema marshmallow
        :return:
        """
        name_schema = create_schema_name(schema)
        if name_schema not in self.spec_schemas and name_schema not in self.spec.components.schemas:
            self.spec_schemas[name_schema] = schema
            if APISPEC_VERSION_MAJOR < 1:
                self.spec.definition(name_schema, schema=schema)
            else:
                self.spec.components.schema(name_schema, schema=schema)

    def _add_tags_in_spec(self, tag: Dict[str, str]) -> None:
        """
        Add tags in spec
        :param tag: {'name': '<name tag>', 'description': '<tag description>', 'add_in_spec': <added tag in spec?>}
        :return:
        """
        if tag.get("add_in_spec", True) is False:
            self.spec_tag[tag["name"]]["add_in_spec"] = True
            tag_in_spec = {
                "name": tag["name"],
                "description": tag["description"]
            }
            if APISPEC_VERSION_MAJOR < 1:
                self.spec.add_tag(tag_in_spec)
            else:
                self.spec.tag(tag_in_spec)
Exemplo n.º 2
0
class Spec(object):
    title = 'Swagman'
    version = '1.0.0'
    description = 'A sample description'
    openapi_version = '3.0.0'

    def __init__(self, ignoreschema=None, **options):
        self.spec = APISpec(title=self.title,
                            version=self.version,
                            openapi_version=self.openapi_version,
                            plugins=[],
                            **options)
        self.ignoreschema = ignoreschema
        self._counter = {'example': {}, 'schema': {}}
        self._examples = {}

    def set_title(self, title=None):
        self.spec.title = title or self.title

    def set_version(self, version=None):
        self.spec.version = version or self.version

    def set_description(self, description=''):
        self.spec.options['info'] = dict(
            description=(description or self.description))

    def add_component_response(self, name, schema):
        self.spec.components.response(name, schema)
        return self

    def add_component_example(self, name, schema):
        if self._counter['example'].get(name, None) is None:
            self._counter['example'] = {name: 0}
            _name = name
        else:
            self._counter['example'][name] += 1
            _name = name + '_' + str(self._counter['example'][name])
        try:
            self.spec.components.example(_name, schema)
        except DuplicateComponentNameError as e:
            # new schema has same repr?
            self.add_component_example(name, schema)
        return self

    def add_component_schema(self, name, schema):
        if self._counter['schema'].get(name, None) is None:
            self._counter['schema'] = {name: 0}
            _name = name
        else:
            self._counter['schema'][name] += 1
            _name = name + '_' + str(self._counter['schema'][name])
        try:
            self.spec.components.schema(_name, schema)
        except DuplicateComponentNameError as e:
            # new schema has same repr?
            self.add_component_schema(name, schema)
        return self

    def get_params(self, request):
        requestparams = []
        params = request.getParams()

        for location, param in params.items():
            for eachparam in param:
                schema = dict(type='string')
                if eachparam.get('type', None) is not None:
                    schema['type'] = postman_to_openapi_typemap.get(
                        eachparam.get('type'), 'string')
                if eachparam.get('value', None) is not None:
                    schema['default'] = eachparam.get('value')
                    schema['example'] = eachparam.get('value')
                requestparams.append({
                    "in": location,
                    "name": eachparam.get('name', ''),
                    "schema": schema
                })
        return requestparams

    def json_get_path(self, match):
        '''return an iterator based upon MATCH.PATH. Each item is a path component,
    start from outer most item.'''
        if match.context is not None:
            for path_element in self.json_get_path(match.context):
                yield path_element
            yield str(match.path)

    def json_update_path(self, json, path, value):
        '''Update JSON dictionnary PATH with VALUE. Return updated JSON'''
        try:
            first = next(path)
            # check if item is an array
            if first.startswith('[') and first.endswith(']'):
                try:
                    first = int(first[1:-1])
                except ValueError:
                    pass
            json[first] = self.json_update_path(json[first], path, value)
            return json
        except StopIteration:
            return value

    def getFilters(self, path, method, code):
        if not len(self.ignoreschema.keys()):
            return []
        for _path, schemas in self.ignoreschema['schema'].items():
            if PostmanParser.camelize(_path) == path:
                for _method, responsecode in schemas.items():
                    if _method == method:
                        return responsecode.get(code, [])
        return []

    def parse_skip(self, expr):
        type_explode = expr.split(':')
        if len(type_explode) > 1 and type_explode[-1] == 'a':
            return ''.join(type_explode[:-1]), True
        return expr, False

    def filterResponse(self, path, method, code, response):
        responsejson = response.getBody()
        filters = self.getFilters(path, method, code)
        if len(filters) > 0:
            for jsonfilter in filters:
                expr, skip = self.parse_skip(jsonfilter)
                expr = expr if expr else jsonfilter
                jsonpath_expr = jsonpath_rw.parse(expr)
                matches = jsonpath_expr.find(responsejson)
                ignoreprop = PostmanParser.IGNOREPROPKEYVAL if skip else PostmanParser.IGNOREPROP
                for match in matches:
                    responsejson = self.json_update_path(
                        responsejson, self.json_get_path(match), ignoreprop)
        return responsejson

    def get_operations(self, item):
        operations = dict(
            get=dict(responses=dict()),
            post=dict(responses=dict()),
            put=dict(responses=dict()),
            delete=dict(responses=dict()),
            head=dict(responses=dict()),
        )
        camelizeKey = PostmanParser.camelize(
            item['request'].getPathNormalised())
        requestbody = item['request'].getBody()
        requestbodyschema = PostmanParser.schemawalker(requestbody)
        requestbodytype = item['request'].getBodyContent()
        for response in item['responses']:
            if not item['request'].getBody():
                requestbody = response.getRequestBody()
                requestbodyschema = PostmanParser.schemawalker(requestbody)
                requestbodytype = response.getRequestHeader('Content-Type')

            code = response.getCode()
            reqtype = response.getMethod().lower()
            responseBody = self.filterResponse(camelizeKey, reqtype, code,
                                               response)
            responseSchema = PostmanParser.schemawalker(responseBody)
            camelizeKeyExample = PostmanParser.camelize(response.getName())
            ref = self.add_component_schema((camelizeKey + str(code)),
                                            responseSchema)
            if requestbody:
                self.set_example(('request' + camelizeKey), camelizeKeyExample,
                                 dict(value=requestbody))
            self.set_example(
                ('response' + camelizeKey + str(code)), camelizeKeyExample,
                dict(value=response.getBody()))
            operations[reqtype]['operationId'] = camelizeKey + reqtype
            operations[reqtype]['parameters'] = self.get_params(
                item['request'])
            if requestbody:
                operations[reqtype]['requestBody'] = dict(
                    content={
                        requestbodytype:
                        dict(schema=requestbodyschema,
                             examples=self.get_example((
                                 'request' + camelizeKey), camelizeKeyExample))
                    })
            operations[reqtype]['responses'][code] = {
                'description': response.getName(),
                'content': {
                    response.getHeader('Content-Type'): {
                        "schema":
                        self.get_ref('schema', (camelizeKey + str(code))),
                        "examples":
                        self.get_example(
                            ('response' + camelizeKey + str(code)),
                            camelizeKeyExample)
                    }
                }
            }

        # Reset schema counter
        self._counter = {'example': {}, 'schema': {}}
        # Return new dict copy from original, containing only filled responses
        # since python3 doesn't allow mutating dict during iteration, that's
        # the best I can do currently.
        #@TODO fix this please
        newdict = dict()
        for k, v in operations.items():
            if v['responses']:
                newdict[k] = v
        return newdict

    def set_example(self, responseKey, exampleKey, exampleBody):
        if responseKey in self._examples:
            self._examples[responseKey][exampleKey] = exampleBody
        else:
            self._examples[responseKey] = {exampleKey: exampleBody}
        self.add_component_example((responseKey + exampleKey), exampleBody)

    def get_example(self, responseKey, exampleKey):
        examples = self._examples[responseKey]
        for exampleKey, example in examples.items():
            ref = self.get_ref('example', (responseKey + exampleKey))
            examples[exampleKey] = ref
        return examples

    def get_ref(self, holder, name):
        refs = self.spec.get_ref(holder, name)
        counter = self._counter[holder].get(name, 0)
        if counter > 0:
            refs = dict(oneOf=[refs])
            for i in range(1, (counter + 1)):
                ref = self.spec.get_ref(holder, name + '_' + str(i))
                refs['oneOf'].append(ref)
        return refs

    def add_item(self, item):
        operations = self.get_operations(item)
        self.spec.path(path=item['request'].getPathNormalised(),
                       operations=operations)
        return self

    def to_dict(self):
        return self.spec.to_dict()

    def to_yaml(self):
        return self.spec.to_yaml()