def generate_api_docs(filename, api_version, openapi_version): """ Generates swagger (openapi) yaml from routes' docstrings (using apispec library). """ from apispec import APISpec from apispec_webframeworks.flask import FlaskPlugin apidoc = APISpec( title="Grafolean API", version=api_version, openapi_version=openapi_version, plugins=[FlaskPlugin()], info={ "description": "> IMPORTANT: API is under development and is **not final yet** - breaking changes might occur.\n\n" \ "Grafolean is designed API-first. In other words, every functionality of the system is accessible through the API " \ "described below. This allows integration with external systems so that (given the permissions) they too can " \ "enter values, automatically modify entities, set up dashboards... Everything that can be done through frontend " \ "can also be achieved through API.\n\n" \ "This documentation is also available as Swagger/OpenAPI definition: [OAS2](./swagger2.yml) / [OAS3](./swagger3.yml)." } ) for apidoc_schemas_func in [users_apidoc_schemas, accounts_apidoc_schemas, admin_apidoc_schemas]: for schema_name, schema in apidoc_schemas_func(): apidoc.components.schema(schema_name, schema) with app.test_request_context(): for rule in app.url_map.iter_rules(): view = app.view_functions.get(rule.endpoint) apidoc.path(view=view) with open(filename, 'w') as openapi_yaml_file: openapi_yaml_file.write(apidoc.to_yaml())
def generate_swagger() -> str: # Create an APISpec spec = APISpec( title="ARCOR2 Data Models", version=arcor2.api_version(), openapi_version="3.0.2", plugins=[FlaskPlugin(), DataclassesPlugin()], ) # TODO avoid explicit naming of all sub-modules in rpc module for module in (arcor2.data.common, arcor2.data.object_type, rpc.common, rpc.execution, rpc.objects, rpc.robot, rpc.scene, rpc.project, rpc.services, rpc.storage, arcor2.data.events): for name, obj in inspect.getmembers(module): if not inspect.isclass(obj) or not issubclass( obj, JsonSchemaMixin) or obj == JsonSchemaMixin: continue try: spec.components.schema(obj.__name__, schema=obj) except DuplicateComponentNameError: continue return spec.to_yaml()
class Spec: def __init__(self, api, title, version): self.obj = API_Spec(title=title, version=version, openapi_version='2.0', produces=['application/json'], consumes=['application/json'], tags=rc_tags, plugins=[Falcon_Plugin(api), Marshmallow_Plugin(schema_name_resolver=self.__schema_name_resolver)]) def get(self): return self.obj def write(self): path = Path(__file__).parent / '../swagger/schema.yaml' with path.open('w') as file: file.write(self.obj.to_yaml()) path = Path(__file__).parent / '../swagger/schema.json' with path.open('w') as file: file.write(dumps(self.obj.to_dict(), indent=2)) @staticmethod def __schema_name_resolver(schema): if is_str(schema): ref = schema else: ref = schema.__class__.__name__ return ref.replace('_Schema', '')
def write_yaml_file(spec: APISpec): """ Экспортируем объект APISpec в YAML файл. :param spec: объект APISpec """ with open(DOCS_FILENAME, 'w') as file: file.write(spec.to_yaml()) print(f'Сохранили документацию в {DOCS_FILENAME}')
def run_app( app: Flask, name: str, version: str, api_version: str, port: int, dataclasses: Optional[List[Type[JsonSchemaMixin]]] = None, print_spec: bool = False, ) -> None: spec = APISpec( title=f"{name} ({version})", version=api_version, openapi_version="3.0.2", plugins=[FlaskPlugin(), DataclassesPlugin()], ) if dataclasses is not None: for dc in dataclasses: spec.components.schema(dc.__name__, schema=dc) with app.test_request_context(): for rule in app.url_map.iter_rules(): if rule.endpoint != "static": spec.path(view=app.view_functions[rule.endpoint]) if print_spec: print(spec.to_yaml()) return @app.route("/swagger/api/swagger.json", methods=["GET"]) def get_swagger() -> str: return jsonify(spec.to_dict()) @app.errorhandler(Arcor2Exception) def handle_bad_request_general(e: Arcor2Exception) -> Tuple[str, int]: return json.dumps(str(e)), 400 @app.errorhandler(FlaskException) def handle_bad_request_intentional(e: FlaskException) -> Tuple[str, int]: return json.dumps(str(e)), e.error_code SWAGGER_URL = "/swagger" swaggerui_blueprint = get_swaggerui_blueprint( SWAGGER_URL, "./api/swagger.json" # Swagger UI static files will be mapped to '{SWAGGER_URL}/dist/' ) # Register blueprint at URL app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) if not env.get_bool("ARCOR2_REST_API_DEBUG", False): # turn off logging each endpoint call by default log = logging.getLogger("werkzeug") log.setLevel(logging.ERROR) app.run(host="0.0.0.0", port=port)
def convert(self, yaml=False, references=True, ignorespec=None, **options): spec = APISpec(title=self.info.get('title'), version=self.info.get('version'), openapi_version=self.openapi, plugins=[MultiOperationBuilderPlugin(references)], **options) for requestitem in self.postman_collection.get_requestitems(): spec.path(path=requestitem.get_request().path, operations=Operation(requestitem, ignorespec=ignorespec).get()) return spec.to_yaml() if yaml else spec.to_dict()
def openapi_format(format="yaml", server="localhost", no_servers=False): extra_specs = { 'info': { 'description': 'The Faraday REST API enables you to interact with ' '[our server](https://github.com/infobyte/faraday).\n' 'Use this API to interact or integrate with Faraday' ' server. This page documents the REST API, with HTTP' ' response codes and example requests and responses.' }, 'security': { "ApiKeyAuth": [] } } if not no_servers: extra_specs['servers'] = [{'url': f'https://{server}/_api'}] spec = APISpec( title="Faraday API", version="2", openapi_version="3.0.2", plugins=[FaradayAPIPlugin(), FlaskPlugin(), MarshmallowPlugin()], **extra_specs) api_key_scheme = { "type": "apiKey", "in": "header", "name": "Authorization" } spec.components.security_scheme("API_KEY", api_key_scheme) response_401_unauthorized = { "description": "You are not authenticated or your API key is missing " "or invalid" } spec.components.response("UnauthorizedError", response_401_unauthorized) with app.test_request_context(): for endpoint in app.view_functions: spec.path(view=app.view_functions[endpoint], app=app) if format.lower() == "yaml": print(spec.to_yaml()) else: print(json.dumps(spec.to_dict(), indent=2))
def generate_swagger_file(): """Automatically generates Swagger spec file based on RequestHandler docstrings and saves it to the specified file_location. """ errors = [] file_location = os.path.join(os.path.dirname(__file__), '..', '..', 'docs', 'api_schema_v{}'.format(API_VERSION)) # Starting to generate Swagger spec file. All the relevant # information can be found from here https://apispec.readthedocs.io/ security_settings = yaml.safe_load(OPENAPI_SPEC_SECURITY) spec = APISpec( title="Unmanic API", version=str(API_VERSION), openapi_version="3.0.0", info=dict(description="Documentation for the Unmanic application API"), plugins=[UnmanicSpecPlugin(), MarshmallowPlugin()], servers=[ { "url": "http://localhost:8888/unmanic/api/v{}/".format(API_VERSION), "description": "Local environment", }, ], **security_settings) # Looping through all the handlers and trying to register them. # Handlers without docstring will raise errors. That's why we # are catching them silently. handlers = find_all_handlers() for handler in handlers: try: spec.path(urlspec=handler) except APISpecError as e: errors.append("API Docs - Failed to append spec path - {}".format( str(e))) pass # Write the Swagger file into specified location. with open('{}.json'.format(file_location), "w", encoding="utf-8") as file: json.dump(spec.to_dict(), file, ensure_ascii=False, indent=4) # TODO: Remove YAML. It sucks! with open('{}.yaml'.format(file_location), "w", encoding="utf-8") as file: file.write(spec.to_yaml()) return errors
def openapi_format(format="yaml"): spec = APISpec( title="Faraday API", version="2", openapi_version="3.0.2", plugins=[FaradayAPIPlugin(), MarshmallowPlugin()], info={'description': 'The Faraday server API'}, ) with app.test_request_context(): for endpoint in app.view_functions: spec.path(view=app.view_functions[endpoint], app=app) if format.lower() == "yaml": print(spec.to_yaml()) else: print(json.dumps(spec.to_dict(), indent=2))
def init_docs(app): ctx = app.test_request_context() ctx.push() settings = yaml.safe_load(OPENAPI_SPEC) # Create an APISpec spec = APISpec( title="Swagger Project", version="1.0.0", openapi_version="3.0.2", plugins=[FlaskPlugin()], **settings ) setup_schema_definition(spec) setup_path(spec) with open("project/docs/swagger.yml", "w") as swagger_file: swagger_file.write(spec.to_yaml()) app.config["SWAGGER"] = {"title": "Swagger Project", "openapi": "3.0.2"} Swagger(app, template=spec.to_dict())
def generate_openapi(service_name: str, version: str, rpcs: List[Type[RPC]], events: List[Type[Event]]) -> str: """Generate OpenAPI models from RPCs and events. Be aware: it modifies __name__ attribute! """ # Create an APISpec spec = APISpec( title=f"{service_name} Data Models", version=version, openapi_version="3.0.2", plugins=[FlaskPlugin(), DataclassesPlugin()], ) for obj in events: if obj is Event: continue _rename_childs(obj) for rpc in rpcs: if rpc is RPC: continue for cls in (rpc.Request, rpc.Response): cls.__name__ = rpc.__name__ + cls.__name__ _rename_childs(cls) for obj in events: _add_to_spec(spec, obj) for rpc in rpcs: for cls in (rpc.Request, rpc.Response): _add_to_spec(spec, cls) return spec.to_yaml()
class OpenAPISpec: def __init__(self, *args, **kwargs): self.spec = APISpec(*args, **kwargs) @property def to_yaml(self): return self.spec.to_yaml() @property def to_dict(self): return self.spec.to_dict() @property def components(self): return self.spec.components def path(self, path): def wrapped(func): self.spec.path(path, description=func.__doc__.partition('\n')[0], operations=load_yaml_from_docstring(func.__doc__)) return func return wrapped
def openapi_format(format="yaml", server="localhost", no_servers=False, return_tags=False): extra_specs = { 'info': { 'description': 'The Faraday REST API enables you to interact with ' '[our server](https://github.com/infobyte/faraday).\n' 'Use this API to interact or integrate with Faraday' ' server. This page documents the REST API, with HTTP' ' response codes and example requests and responses.' }, 'security': { "ApiKeyAuth": [] } } if not no_servers: extra_specs['servers'] = [{'url': f'https://{server}/_api'}] spec = APISpec( title="Faraday API", version="2", openapi_version="3.0.2", plugins=[FaradayAPIPlugin(), FlaskPlugin(), MarshmallowPlugin()], **extra_specs) api_key_scheme = { "type": "apiKey", "in": "header", "name": "Authorization" } spec.components.security_scheme("API_KEY", api_key_scheme) response_401_unauthorized = { "description": "You are not authenticated or your API key is missing " "or invalid" } spec.components.response("UnauthorizedError", response_401_unauthorized) tags = set() with get_app().test_request_context(): for endpoint in get_app().view_functions.values(): spec.path(view=endpoint, app=get_app()) # Set up global tags spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.SafeLoader) for path_value in spec_yaml["paths"].values(): for data_value in path_value.values(): if 'tags' in data_value and any(data_value['tags']): for tag in data_value['tags']: tags.add(tag) for tag in sorted(tags): spec.tag({'name': tag}) if return_tags: return sorted(tags) if format.lower() == "yaml": print(spec.to_yaml()) else: print(json.dumps(spec.to_dict(), indent=2))
200: description: A pet to be returned schema: PetSchema 400: x-400-suffix: yeah yeah.... """ pass # Optional marshmallow support class CategorySchema(Schema): id = fields.Int() name = fields.Str(required=True) class PetSchema(Schema): category = fields.Nested(CategorySchema, many=True) name = fields.Str() spec.components.schema('Category', schema=CategorySchema) spec.components.schema('Pet', schema=PetSchema) with app.test_request_context(): spec.path(view=random_pet) if __name__ == "__main__": # This should match extended_example_with_marshmallow_output.yaml print(spec.to_yaml())
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()
def generate(app, api_commit_hash, api_semantic_version): ########################### ##### API SPEC ############ # API spec is autogenerated using the 'api-spec' library, and saved in the project root # as well as being served on the root '/' endpoint for consumption by services spec = APISpec( title="RCPCH Digital Growth Charts API", version=f'v{api_semantic_version} (commit_hash: {api_commit_hash})', openapi_version="3.0.2", info=dict( description= "Royal College of Paediatrics and Child Health Digital Growth Charts", license={ "name": "GNU Affero General Public License", "url": "https://www.gnu.org/licenses/agpl-3.0.en.html" }), plugins=[MarshmallowPlugin(), FlaskPlugin()], servers=[{ "url": 'https://api.rcpch.ac.uk', "description": 'RCPCH Production API Gateway (subscription keys required)' }, { "url": 'https://localhost:5000', "description": 'Your local development API' }], ) spec.components.schema("uk_who_calculation", schema=schemas.CalculationResponseSchema) with app.test_request_context(): spec.path(view=blueprints.uk_who_blueprint.uk_who_calculation) spec.components.schema("chartData", schema=schemas.ChartDataResponseSchema) with app.test_request_context(): spec.path(view=blueprints.uk_who_blueprint.uk_who_chart_coordinates) spec.components.schema("plottableChildData", schema=schemas.PlottableChildDataResponseSchema) with app.test_request_context(): spec.path(view=blueprints.uk_who_blueprint.uk_who_plottable_child_data) spec.components.schema("references", schema=schemas.ReferencesResponseSchema) with app.test_request_context(): spec.path(view=blueprints.utilities_blueprint.references) # Instructions endpoint (TODO: #121 #120 candidate for deprecation) with app.test_request_context(): spec.path(view=blueprints.utilities_blueprint.instructions) # Trisomy 21 endpoint spec.components.schema("trisomy_21_calculation", schema=schemas.CalculationResponseSchema) with app.test_request_context(): spec.path(view=blueprints.trisomy_21_blueprint.trisomy_21_calculation) # OpenAPI3 specification endpoint with app.test_request_context(): spec.path(view=blueprints.openapi_blueprint.openapi_endpoint) # Turner's syndrome endpoint spec.components.schema("turner_calculation", schema=schemas.CalculationResponseSchema) with app.test_request_context(): spec.path(view=blueprints.turner_blueprint.turner_calculation) ##### END API SPEC ######## ########################### ################################ ### API SPEC AUTO GENERATION ### # Create OpenAPI Spec and serialise it to file with open(r'openapi.yml', 'w') as file: file.write(spec.to_yaml()) with open(r'openapi.json', 'w') as file: file.write(json.dumps(spec.to_dict(), sort_keys=True, indent=4)) ### END API SPEC AUTO GENERATION ### #################################### return spec
from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin from model_service import __doc__ from model_service.schemas import * from model_service.endpoints import * spec = APISpec( openapi_version="3.0.2", title='Model Service', version='0.1.0', info=dict(description=__doc__), plugins=[FlaskPlugin(), MarshmallowPlugin()], ) spec.components.schema("ModelSchema", schema=ModelSchema) spec.components.schema("ModelCollectionSchema", schema=ModelCollectionSchema) spec.components.schema("JsonSchemaProperty", schema=JsonSchemaProperty) spec.components.schema("JSONSchema", schema=JSONSchema) spec.components.schema("ModelMetadataSchema", schema=ModelMetadataSchema) spec.components.schema("ErrorSchema", schema=ErrorSchema) with app.test_request_context(): spec.path(view=get_models) spec.path(view=get_metadata) spec.path(view=predict) with open('../openapi_specification.yaml', 'w') as f: f.write(spec.to_yaml())
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()
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()
}, ) specification.path( path="/orders/{order_id}", parameters=[make_parameter(in_="path", name="order_id", schema={"type": "string"})], operations={ "get": { "summary": "Returns the details of a specific order", "responses": make_response( "GetOrder", description="A JSON representation of an order" ), }, "put": { "description": "Replaces an existing order", "requestBody": make_request_body("CreateOrder"), "responses": make_response( "GetOrder", description="A JSON representation of an order" ), }, "delete": { "description": "Deletes an existing order", "responses": { "204": {"description": "The resource was deleted successfully"} }, }, }, ) print(specification.to_yaml())