Exemplo n.º 1
0
class WRAPISpec(object):
    RE_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')

    tags = [
        {
            'name': 'WASAPI (Downloads)',
            'description': 'Download WARC files API (conforms to WASAPI spec)'
        },
        {
            'name': 'Auth',
            'description': 'Auth and Login API'
        },
        {
            'name': 'Users',
            'description': 'User API'
        },
        {
            'name': 'Collections',
            'description': 'Collection API'
        },
        {
            'name': 'Recordings',
            'description': 'Recording Sessions Management API'
        },
        {
            'name': 'Lists',
            'description': 'List API'
        },
        {
            'name': 'Bookmarks',
            'description': 'Bookmarks API'
        },
        {
            'name': 'Uploads',
            'description': 'Upload WARC or HAR files API'
        },
        {
            'name': 'Add External Records',
            'description': 'Add External WARC Records API'
        },
        {
            'name': 'Browsers',
            'description': 'Browser API'
        },
        {
            'name': 'External Archives',
            'description': 'External Archives Info API'
        },
        {
            'name': 'Cookies',
            'description': 'Cookie Handling'
        },
        {
            'name': 'Bug Reporting',
            'description': 'Bug Reporting API'
        },
        {
            'name': 'Admin',
            'description': 'Admin API',
        },
        {
            'name': 'Stats',
            'description': 'Stats API',
        },
        {
            'name': 'Automation',
            'description': 'Automation API',
        },
        {
            'name': 'Behaviors',
            'description': 'Behaviors API'
        },
    ]

    # only include these groups when logged in as admin
    admin_tags = ['Admin', 'Stats', 'Automation']

    string_params = {
        'user': '******',
        'username': '******',
        'coll': 'Collection Slug',
        'coll_name': 'Collection Slug',
        'collection': 'Collection Slug',
        'rec': 'Session Id',
        'reqid': 'Remote Browser Request Id',
        'new_coll_name': 'New Collection Name',
        'list': 'List Id',
        'list_id': 'List Id',
        'bid': 'Bookmark Id',
        'autoid': 'Automation Id',
        'title': 'Title',
        'desc': 'Description',
        'url': 'Archived Url',
        'timestamp': 'Archived at Timestamp',
        'browser': 'Browser Used',
        'page_id': 'Page Id',
        'upload_id': 'Upload Id',
        'filename': 'File Name',
    }

    opt_bool_params = {
        'public': 'Publicly Accessible',
        'include_recordings': 'Include Recording Sessions in response',
        'include_lists': 'Include all lists in response',
        'include_pages': 'Include pages in response',
        'include_bookmarks': 'Include bookmarks in response',
        'public_index': 'Publicly Accessible Collection Index',
    }

    custom_params = {
        'before_id': {
            'type': 'string',
            'description': 'Insert Before this Id',
        },
        'order': {
            'type': 'array',
            'items': {
                'type': 'string'
            },
            'description': 'an array of existing ids in new order'
        }
    }

    all_responses = {
        'wasapi_list': {
            'description':
            'WASAPI response for list of WARC files available for download',
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'files': {
                                'type': 'array',
                                'items': {
                                    'type': 'object',
                                    'properties': {
                                        'content-type': {
                                            'type': 'string'
                                        },
                                        'filetype': {
                                            'type': 'string'
                                        },
                                        'filename': {
                                            'type': 'string',
                                        },
                                        'size': {
                                            'type': 'integer'
                                        },
                                        'recording': {
                                            'type': 'string'
                                        },
                                        'recording_date': {
                                            'type': 'string'
                                        },
                                        'collection': {
                                            'type': 'string'
                                        },
                                        'checksums': {
                                            'type': 'object'
                                        },
                                        'locations': {
                                            'type': 'array',
                                            'items': {
                                                'type': 'string'
                                            }
                                        },
                                        'is_active': {
                                            'type': 'boolean'
                                        },
                                    }
                                }
                            },
                            'include-extra': {
                                'type': 'boolean'
                            }
                        }
                    }
                }
            }
        },
        'wasapi_download': {
            'description': 'WARC file',
            'content': {
                'application/warc': {
                    'schema': {
                        'type': 'string',
                        'format': 'binary',
                        'example': 'WARC/1.0\r\nWARC-Type: response\r\n...',
                    }
                }
            }
        }
    }

    @classmethod
    def bottle_path_to_openapi(cls, path):
        path_vars = cls.RE_URL.findall(path)
        path = cls.RE_URL.sub(r'{\1}', path)
        return path, path_vars

    def __init__(self, api_root):
        self.api_root = api_root
        self.api_map = defaultdict(dict)
        self.funcs = defaultdict(dict)

        self.curr_tag = ''

        self.spec = APISpec(
            title='Webrecorder',
            version='1.0.0',
            openapi_version='3.0.0',
            info=dict(
                description=
                'Webrecorder API. This API includes all features available and in use by the frontend.'
            ),
            plugins=[])

        self.admin_spec = APISpec(
            title='Webrecorder',
            version='1.0.0',
            openapi_version='3.0.0',
            info=dict(
                description=
                'Webrecorder API (including Admin). This API includes all features available in Webrecorder, including admin and stats APIs.'
            ),
            plugins=[])

        self.err_400 = self.make_err_response('Invalid Request Param')
        self.err_404 = self.make_err_response('Object Not Found')
        self.err_403 = self.make_err_response('Invalid Authorization')
        self.any_obj = self.make_any_response()

    def set_curr_tag(self, tag):
        self.curr_tag = tag

    def add_route(self, route):
        if route.rule.startswith(self.api_root):
            path, path_vars = self.bottle_path_to_openapi(route.rule)

            self.api_map[path][route.method.lower()] = route.callback

            self.funcs[route.callback]['path'] = path,
            self.funcs[route.callback]['path_params'] = self.make_params(
                path_vars, 'path')
            if self.curr_tag:
                self.funcs[route.callback]['tags'] = [self.curr_tag]

    def get_param(self, name):
        """Returns the open api description of the supplied query parameter name

        :param str name: The name of the query parameter
        :return: A dictionary containing the
        :rtype: dict
        """
        optional = name.startswith('?')
        if optional:
            name = name[1:]
        if name in self.string_params:
            param = {
                'description': self.string_params[name],
                'required': not optional,
                'schema': {
                    'type': 'string'
                },
                'name': name
            }

        elif name in self.opt_bool_params:
            param = {
                'description': self.opt_bool_params[name],
                'required': False,
                'schema': {
                    'type': 'boolean'
                },
                'name': name
            }

        elif name in self.custom_params:
            param = self.custom_params[name].copy()
            param['name'] = name

        else:
            raise AssertionError('Param {0} not found'.format(name))

        return param

    def get_req_param(self, name):
        if name in self.string_params:
            return {'type': 'string', 'description': self.string_params[name]}

        elif name in self.opt_bool_params:
            return {
                'type': 'boolean',
                'description': self.opt_bool_params[name]
            }

        elif name in self.custom_params:
            return self.custom_params[name]

        raise AssertionError('Param {0} not found'.format(name))

    def make_params(self, params, param_type):
        objs = []
        for param in params:
            obj = self.get_param(param)
            obj['in'] = param_type
            objs.append(obj)

        return objs

    def add_func(self, func, kwargs):
        query = kwargs.get('query')
        if query:
            self.funcs[func]['query_params'] = self.make_params(query, 'query')

        req = kwargs.get('req')
        if req:
            self.funcs[func]['request'] = self.get_request(
                req, kwargs.get('req_desc'))

        resp = kwargs.get('resp')
        if resp:
            self.funcs[func]['resp'] = resp

    def get_request(self, req_props, req_desc=None):
        properties = {}

        schema = None

        # make array out of props
        if isinstance(req_props, dict):
            if req_props.get('type') == 'array':
                obj_type = 'array'
                prop_list = req_props['item_type']

            assert (prop_list)

        else:
            obj_type = 'object'
            prop_list = req_props

        if not schema:
            for prop in prop_list:
                properties[prop] = self.get_req_param(prop)

            schema = {'type': 'object', 'properties': properties}

            # wrap schema in array
            if obj_type == 'array':
                schema = {'type': 'array', 'items': schema}

        request = {'content': {'application/json': {'schema': schema}}}

        if req_desc:
            request['description'] = req_desc

        return request

    def build_api_spec(self):
        for name, routes in self.api_map.items():
            ops = {}
            for method, callback in routes.items():
                info = self.funcs[callback]

                # combine path params and query params, if any
                params = info.get('path_params', []) + info.get(
                    'query_params', [])

                api = {'parameters': params}

                # for POST and PUT, generate requestBody
                if method == 'post' or method == 'put':
                    request = info.get('request')
                    if request:
                        api['requestBody'] = request
                else:
                    # otherwise, ensure no request body!
                    assert 'request' not in info

                # set tags, if any
                if 'tags' in info:
                    api['tags'] = info['tags']
                    is_admin = info['tags'][0] in self.admin_tags

                api['responses'] = self.get_responses(info.get('resp', None))

                ops[method] = api

            if not is_admin:
                self.spec.add_path(path=name, operations=ops)

            self.admin_spec.add_path(path=name, operations=ops)

        for tag in self.tags:
            self.admin_spec.add_tag(tag)

            if tag['name'] not in self.admin_tags:
                self.spec.add_tag(tag)
            else:
                print('skip', tag)

    def get_responses(self, obj_type):
        response_obj = self.all_responses.get(obj_type) or self.any_obj
        obj = {'400': self.err_400, '404': self.err_404, '200': response_obj}

        return obj

    def make_err_response(self, msg):
        obj = {
            'description': msg,
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'error': {
                                'type': 'string'
                            }
                        }
                    }
                }
            }
        }

        return obj

    def make_any_response(self):
        obj = {
            'description': 'Any Object',
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'additionalProperties': True
                    }
                }
            }
        }

        return obj

    def get_api_spec_yaml(self, use_admin=False):
        """Returns the api specification as a yaml string

        :return: The api specification as a yaml string
        :rtype: str
        """
        return self.spec.to_yaml(
        ) if not use_admin else self.admin_spec.to_yaml()

    def get_api_spec_dict(self, use_admin=False):
        """Returns the api specification as a dictionary

        :return: The api specification as a dictionary
        :rtype: dict
        """
        return self.spec.to_dict(
        ) if not use_admin else self.admin_spec.to_dict()
Exemplo n.º 2
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 = {"$ref": f"#/definitions/{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.º 3
0

# Swagger API
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin

# Create an APISpec
spec = APISpec(
    title='Feature Requests',
    version='0.1.0b1',
    openapi_version='2.0',
    plugins=[
        MarshmallowPlugin(),
        'apispec_flask_restful',
    ],
)
spec.add_tag({'name': 'featureRequest'})
spec.add_tag({'name': 'productArea'})
spec.add_tag({'name': 'client'})
spec.definition('FeatureRequest', schema=FeatureRequestSchema)
spec.definition('Client', schema=ClientSchema)
spec.definition('ProductArea', schema=ProductAreaSchema)
spec.add_path(resource=FeatureRequestListResource, api=api)
spec.add_path(resource=ClientResource, api=api)
spec.add_path(resource=ProductAreaResource, api=api)

@app.route('/v1')
def swagger():
    return jsonify(spec.to_dict())

@app.cli.command('swagger')
def show_swagger_yaml():
Exemplo n.º 4
0
class WRAPISpec(object):
    RE_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')

    tags = [
        {
            'name': 'Auth',
            'description': 'Auth and Login API'
        },
        {
            'name': 'Users',
            'description': 'User API'
        },
        {
            'name': 'Collections',
            'description': 'Collection API'
        },
        {
            'name': 'Recordings',
            'description': 'Recording Sessions Management API'
        },
        {
            'name': 'Lists',
            'description': 'List API'
        },
        {
            'name': 'Bookmarks',
            'description': 'Bookmarks API'
        },
        {
            'name': 'Browsers',
            'description': 'Browser API'
        },
        {
            'name': 'External Archives',
            'description': 'External Archives Info API'
        },
        {
            'name': 'Cookies',
            'description': 'Cookie Handling'
        },
        {
            'name': 'Bug Reporting',
            'description': 'Bug Reporting API'
        },
        {
            'name': 'Admin',
            'description': 'Admin API'
        },
        {
            'name': 'Stats',
            'description': 'Stats API'
        },
    ]

    string_params = {
        'user': '******',
        'username': '******',
        'coll': 'Collection Slug',
        'coll_name': 'Collection Slug',
        'rec': 'Session Id',
        'reqid': 'Remote Browser Request Id',
        'new_coll_name': 'New Collection Name',
        'list': 'List Id',
        'list_id': 'List Id',
        'bid': 'Bookmark Id',
        'title': 'Title',
        'desc': 'Description',
        'url': 'Archived Url',
        'timestamp': 'Archived at Timestamp',
        'browser': 'Browser Used',
        'page_id': 'Page Id',
        'upload_id': 'Upload Id',
    }

    opt_bool_params = {
        'public': 'Publicly Accessible',
        'include_recordings': 'Include Recording Sessions in response',
        'include_lists': 'Include all lists in response',
        'include_pages': 'Include pages in response',
        'include_bookmarks': 'Include bookmarks in response',
        'public_index': 'Publicly Accessible Collection Index',
    }

    custom_params = {
        'before_id': {
            'type': 'string',
            'description': 'Insert Before this Id',
        },
        'order': {
            'type': 'array',
            'items': {
                'type': 'string'
            },
            'description': 'an array of existing ids in new order'
        }
    }

    all_responses = {}

    @classmethod
    def bottle_path_to_openapi(cls, path):
        path_vars = cls.RE_URL.findall(path)
        path = cls.RE_URL.sub(r'{\1}', path)
        return path, path_vars

    def __init__(self, api_root):
        self.api_root = api_root
        self.api_map = defaultdict(dict)
        self.funcs = defaultdict(dict)

        self.curr_tag = ''

        self.spec = APISpec(title='Webrecorder',
                            version='1.0.0',
                            openapi_version='3.0.0',
                            info=dict(description='Webrecorder API'),
                            plugins=[])

        self.err_400 = self.make_err_response('Invalid Request Param')
        self.err_404 = self.make_err_response('Object Not Found')
        self.err_403 = self.make_err_response('Invalid Authorization')
        self.any_obj = self.make_any_response()

    def set_curr_tag(self, tag):
        self.curr_tag = tag

    def add_route(self, route):
        if route.rule.startswith(self.api_root):
            path, path_vars = self.bottle_path_to_openapi(route.rule)

            self.api_map[path][route.method.lower()] = route.callback

            self.funcs[route.callback]['path'] = path,
            self.funcs[route.callback]['path_params'] = self.make_params(
                path_vars, 'path')
            if self.curr_tag:
                self.funcs[route.callback]['tags'] = [self.curr_tag]

    def get_param(self, name):
        if name in self.string_params:
            param = {
                'description': self.string_params[name],
                'required': True,
                'schema': {
                    'type': 'string'
                },
                'name': name
            }

        elif name in self.opt_bool_params:
            param = {
                'description': self.opt_bool_params[name],
                'required': False,
                'schema': {
                    'type': 'boolean'
                },
                'name': name
            }

        elif name in self.custom_params:
            param = self.custom_params[name].copy()
            param['name'] = name

        else:
            raise AssertionError('Param {0} not found'.format(name))

        return param

    def get_req_param(self, name):
        if name in self.string_params:
            return {'type': 'string', 'description': self.string_params[name]}

        elif name in self.opt_bool_params:
            return {
                'type': 'boolean',
                'description': self.opt_bool_params[name]
            }

        elif name in self.custom_params:
            return self.custom_params[name]

        raise AssertionError('Param {0} not found'.format(name))

    def make_params(self, params, param_type):
        objs = []
        for param in params:
            obj = self.get_param(param)
            obj['in'] = param_type
            objs.append(obj)

        return objs

    def add_func(self, func, kwargs):
        query = kwargs.get('query')
        if query:
            self.funcs[func]['query_params'] = self.make_params(query, 'query')

        req = kwargs.get('req')
        if req:
            self.funcs[func]['request'] = self.get_request(
                req, kwargs.get('req_desc'))

    def get_request(self, req_props, req_desc=None):
        properties = {}

        schema = None

        # make array out of props
        if isinstance(req_props, dict):
            if req_props.get('type') == 'array':
                obj_type = 'array'
                prop_list = req_props['item_type']

            assert (prop_list)

        else:
            obj_type = 'object'
            prop_list = req_props

        if not schema:
            for prop in prop_list:
                properties[prop] = self.get_req_param(prop)

            schema = {'type': 'object', 'properties': properties}

            # wrap schema in array
            if obj_type == 'array':
                schema = {'type': 'array', 'items': schema}

        request = {'content': {'application/json': {'schema': schema}}}

        if req_desc:
            request['description'] = req_desc

        return request

    def build_api_spec(self):
        for name, routes in self.api_map.items():
            ops = {}
            for method, callback in routes.items():
                info = self.funcs[callback]

                # combine path params and query params, if any
                params = info.get('path_params', []) + info.get(
                    'query_params', [])

                api = {'parameters': params}

                # for POST and PUT, generate requestBody
                if method == 'post' or method == 'put':
                    request = info.get('request')
                    if request:
                        api['requestBody'] = request
                else:
                    # otherwise, ensure no request body!
                    assert 'request' not in info

                # set tags, if any
                if 'tags' in info:
                    api['tags'] = info['tags']

                api['responses'] = self.get_responses(None)

                ops[method] = api

            self.spec.add_path(path=name, operations=ops)

        for tag in self.tags:
            self.spec.add_tag(tag)

    def get_responses(self, obj_type):
        response_obj = self.all_responses.get(obj_type) or self.any_obj
        obj = {'400': self.err_400, '404': self.err_404, '200': response_obj}

        return obj

    def make_err_response(self, msg):
        obj = {
            'description': msg,
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'error': {
                                'type': 'string'
                            }
                        }
                    }
                }
            }
        }

        return obj

    def make_any_response(self):
        obj = {
            'description': 'Any Object',
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'additionalProperties': True
                    }
                }
            }
        }

        return obj

    def get_api_spec_yaml(self):
        return self.spec.to_yaml()
Exemplo n.º 5
0
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from sanicplugin import SanicPlugin
from app.api.v1.views.cart_view import UserView
from app.api.v1.models.cart import UserSchema

# @TODO: Add tests in CI for successful generation of APISpec file
# Create spec object
spec = APISpec(
    title='StarScream',
    version='1.0.0',
    openapi_version='2.0',
    info=dict(
        description='Check status by pinging the server'
    ),
    plugins=[MarshmallowPlugin(), SanicPlugin()]
)

# Add tags
spec.add_tag(
    {"name": "user", "description": "Everything you can do with a user"})
# Add definitions
spec.definition('User', schema=UserSchema)
# Add views to generate paths for
user_view = UserView.as_view('user')
spec.add_path(path='/user', view=user_view)

# Save this generated spec to a file for now.
with open('swagger.json', 'w') as f:
    json.dump(spec.to_dict(), f)