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()
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)
# 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():
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()
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)