def path_helper(self, operations, resource, base_path=None, **kwargs): # noqa the signature does not match as custom kwargs are used """Path helper that allows passing a Falcon resource instance.""" resource_uri_mapping = self._get_resource_uri_mapping() if resource not in resource_uri_mapping: raise APISpecError(f"Could not find endpoint for resource {resource}") operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) path = resource_uri_mapping[resource]["uri"] if base_path is not None: # make sure base_path accept either with or without leading slash # swagger 2 usually come with leading slash but not in openapi 3.x.x base_path = '/' + base_path.strip('/') path = re.sub(base_path, "", path, 1) methods = resource_uri_mapping[resource]["methods"] for method_name, method_handler in methods.items(): docstring_yaml = yaml_utils.load_yaml_from_docstring(method_handler.__doc__) operations[method_name] = docstring_yaml or dict() return path
def operation_helper(self, path=None, operations=None, **kwargs): """Если для query параметров указали схему marshmallow, то раскрываем её и вытаскиваем параметры первого уровня, без Nested""" resource = kwargs.get("resource", None) for m in getattr(resource, "methods", []): m = m.lower() f = getattr(resource, m) m_ops = load_yaml_from_docstring(f.__doc__) if m_ops: operations.update({m: m_ops}) self._ref_to_spec(m_ops) for method, val in operations.items(): for index, parametr in enumerate( val["parameters"] if "parameters" in val else []): if "in" in parametr and parametr[ "in"] == "query" and "schema" in parametr: name_schema = parametr["schema"]["$ref"].split("/")[-1] new_parameters = [] name_schema = create_schema_name(name_schema=name_schema) if name_schema in self.spec.components._schemas: for i_name, i_value in self.spec.components._schemas[ name_schema]["properties"].items(): new_parameter = { "name": i_name, "in": "query", "type": i_value.get("type"), "description": i_value.get("description", ""), } if "items" in i_value: new_items = { "type": i_value["items"].get("type"), } if "enum" in i_value["items"]: new_items["enum"] = i_value["items"][ "enum"] new_parameter.update({"items": new_items}) new_parameters.append(new_parameter) del val["parameters"][index] val["parameters"].extend(new_parameters)
def path_helper(self, path=None, operations: dict = None, parameters: list = None, **kwargs): """Path helper that allows passing a Falcon resource instance.""" uri_to_method_map = self._get_uri_falcon_details_mapping() if path not in uri_to_method_map: raise APISpecError(f"Could not find handlers for path='{path}'") falcon_routing_details = uri_to_method_map[path] resource = falcon_routing_details["resource"] operations.update( yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) methods = falcon_routing_details["methods"] for method_name, method_handler in methods.items(): docstring_yaml = yaml_utils.load_yaml_from_docstring( method_handler.__doc__) operations[method_name] = docstring_yaml or dict() return path
def operation_helper(self, path=None, operations=None, **kwargs): """ If query params have a marshmallow schema reference, get params from the schema from the top level (no Nested) """ resource = kwargs.get("resource", None) for m in getattr(resource, "methods", []): m = m.lower() f = getattr(resource, m) m_ops = load_yaml_from_docstring(f.__doc__) if m_ops: operations.update({m: m_ops}) self._ref_to_spec(m_ops) for method, val in operations.items(): for index, parametr in enumerate( val["parameters"] if "parameters" in val else []): if ("in" in parametr and parametr["in"] == "query" and "schema" in parametr and "$ref" in parametr["schema"]): name_schema = parametr["schema"]["$ref"].split("/")[-1] new_parameters = self.process_query_params_spec( name_schema) del val["parameters"][index] val["parameters"].extend(new_parameters)
def path_helper(self, operations, resource, base_path=None, **kwargs): """Path helper that allows passing a Falcon resource instance.""" resource_uri_mapping = self._generate_resource_uri_mapping(self._app) if resource not in resource_uri_mapping: raise APISpecError("Could not find endpoint for resource {0}".format(resource)) operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) path = resource_uri_mapping[resource] if base_path is not None: # make sure base_path accept either with or without leading slash # swagger 2 usually come with leading slash but not in openapi 3.x.x base_path = '/' + base_path.strip('/') path = re.sub(base_path, "", path, 1) for method in falcon.constants.HTTP_METHODS: http_verb = method.lower() method_name = "on_" + http_verb if getattr(resource, method_name, None) is not None: method = getattr(resource, method_name) docstring_yaml = yaml_utils.load_yaml_from_docstring(method.__doc__) operations[http_verb] = docstring_yaml or dict() return path
def test_load_yaml_from_docstring_empty_docstring(docstring): assert yaml_utils.load_yaml_from_docstring(docstring) == {}
def spec_from_handlers(handlers, exclude_internal=True, metadata=None): """Generate an OpenAPI spec from Tornado handlers. The docstrings of the various http methods of the Tornado handlers (`get`, `put`, etc.), should contain OpenAPI yaml after three dashed. E.g.: ```yaml --- description: Retrieve a source parameters: - in: path name: obj_id required: false schema: type: integer required: false responses: 200: content: application/json: schema: oneOf: - SingleSource - Error ``` The yaml snippet may contain two top-level keywords, `single` and `multiple`, that can be used to disambiguate the OpenAPI spec for a single URL that is meant to return both single and multiple objects. E.g., `/api/sources/{obj_id}` may return multiple objects if `{obj_id}` is left unspecified. If these keywords are not specified, the OpenAPI snippet is used as is. Schemas are automatically resolved to matching Marshmallow objects in the `spec` module. E.g., in the above example we use `SingleSource` and `Error`, which refer to `spec.SingleSource` and `spec.Error`. All schemas in `schema` are added to the OpenAPI definition. """ meta = { 'title': 'SkyPortal', 'version': __version__, 'openapi_version': '3.0.2', 'info': { 'description': open(api_description).read(), 'x-logo': { 'url': 'https://raw.githubusercontent.com/skyportal/skyportal/main/static/images/skyportal_logo.png', 'backgroundColor': '#FFFFFF', 'altText': 'SkyPortal logo', 'href': 'https://skyportal.io/docs', }, }, } if metadata is not None: meta.update(metadata) openapi_spec = APISpec( **meta, plugins=[MarshmallowPlugin()], ) token_scheme = { "type": "apiKey", "in": "header", "name": "Authorization", "description": "Header should be in the format 'token abcd-efgh-0000-1234'", } openapi_spec.components.security_scheme("token", token_scheme) schema.register_components(openapi_spec) from apispec import yaml_utils import inspect import re HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch") handlers = [ handler for handler in handlers if not isinstance(handler, URLSpec) and len(handler) == 2 ] if exclude_internal: handlers = [(route, handler_cls) for (route, handler_cls) in handlers if '/internal/' not in route] for (endpoint, handler) in handlers: for http_method in HTTP_METHODS: method = getattr(handler, http_method) if method.__doc__ is None: continue path_template = endpoint path_template = re.sub(r'\(.*?\)\??', '/{}', path_template) path_template = re.sub(r'(/)+', '/', path_template) path_parameters = path_template.count('{}') spec = yaml_utils.load_yaml_from_docstring(method.__doc__) parameters = list(inspect.signature(method).parameters.keys())[1:] parameters = parameters + (path_parameters - len(parameters)) * [ '', ] if parameters[-1:] == [''] and path_template.endswith('/{}'): path_template = path_template[:-3] multiple_spec = spec.pop('multiple', {}) single_spec = spec.pop('single', {}) other_spec = spec for subspec in [single_spec, other_spec]: if subspec: path = path_template.format(*parameters) openapi_spec.path(path=path, operations={http_method: subspec}) if multiple_spec: multiple_path_template = path_template.rsplit('/', 1)[0] multiple_path = multiple_path_template.format(*parameters[:-1]) openapi_spec.path(path=multiple_path, operations={http_method: multiple_spec}) return openapi_spec
def spec_from_handlers(handlers): """Generate an OpenAPI spec from Tornado handlers. The docstrings of the various http methods of the Tornado handlers (`get`, `put`, etc.), should contain OpenAPI yaml after three dashed. E.g.: ```yaml --- description: Retrieve a source parameters: - in: path name: source_id required: false schema: type: integer required: false responses: 200: content: application/json: schema: oneOf: - SingleSource - Error ``` The yaml snippet may contain two top-level keywords, `single` and `multiple`, that can be used to disambiguate the OpenAPI spec for a single URL that is meant to return both single and multiple objects. E.g., `/api/sources/{source_id}` may return multiple objects if `{source_id}` is left unspecified. If these keywords are not specified, the OpenAPI snippet is used as is. Schemas are automatically resolved to matching Marshmallow objects in the `spec` module. E.g., in the above example we use `SingleSource` and `Error`, which refer to `spec.SingleSource` and `spec.Error`. All schemas in `schema` are added to the OpenAPI definition. """ openapi_spec = APISpec(title='SkyPortal', version=__version__, openapi_version='3.0.2', info=dict(description='SkyPortal API'), plugins=[ MarshmallowPlugin(), ]) token_scheme = { "type": "apiKey", "in": "header", "name": "Authorization", "description": "Header should be in the format 'token abcd-efgh-0000-1234'" } openapi_spec.components.security_scheme("token", token_scheme) schema.register_components(openapi_spec) from apispec import yaml_utils import inspect import re HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch") handlers = [ handler for handler in handlers if not isinstance(handler, URLSpec) and len(handler) == 2 ] for (endpoint, handler) in handlers: for http_method in HTTP_METHODS: method = getattr(handler, http_method) if method.__doc__ is None: continue path_template = endpoint path_template = re.sub('\(.*?\)\??', '{}', path_template) path_template = re.sub('(?=[^/]{1}){}', '/{}', path_template) path_parameters = path_template.count('{}') spec = yaml_utils.load_yaml_from_docstring(method.__doc__) parameters = list(inspect.signature(method).parameters.keys())[1:] parameters = parameters + (path_parameters - len(parameters)) * [ '', ] multiple_spec = spec.pop('multiple', {}) single_spec = spec.pop('single', {}) other_spec = spec for subspec in [single_spec, other_spec]: if subspec: path = path_template.format(*parameters) openapi_spec.path(path=path, operations={http_method: subspec}) if multiple_spec: multiple_path_template = path_template.rsplit('/', 1)[0] multiple_path = multiple_path_template.format(*parameters[:-1]) openapi_spec.path(path=multiple_path, operations={http_method: multiple_spec}) return openapi_spec
def update_operations_specs(cls, operations, methods, method=None, detail=None, **specs): operations = operations or {} result = {} for method_name in methods: if method is None and method_name not in cls.methods: continue method_name = method_name.lower() cls_method = method or getattr(cls, method_name, None) if not cls_method: continue defaults = dict(deepcopy(specs)) defaults.setdefault('tags', [cls.meta.name]) default_parameters = cls.build_query_parameters( method_name, detail) if default_parameters: defaults['parameters'] = (defaults.get('parameters') or []) + default_parameters docstring = clean_doc(cls_method.__doc__, cls.__doc__) if docstring: defaults.setdefault('summary', docstring.split('\n')[0]) defaults.setdefault('description', docstring) defaults.setdefault( 'responses', { 200: { 'description': 'OK', 'content': { 'application/json': {} }, 'headers': cls.build_response_headers( method_name, detail) } }) if cls.Schema: schema_name = cls.Schema.__name__.replace('Schema', '') defaults['responses'][200]['content']['application/json']['schema'] \ = {'$ref': '#/components/schemas/%s' % schema_name} if method_name in ('put', 'patch', 'post'): defaults.setdefault('parameters', []) schema = {} if cls.Schema: schema_name = cls.Schema.__name__.replace('Schema', '') schema['$ref'] = '#/components/schemas/%s' % schema_name defaults['requestBody'] = { 'description': 'Request Body', 'required': True, 'content': { 'application/json': { 'schema': schema } } } if method_name in operations: defaults.update(operations[method_name]) docstring_yaml = yaml_utils.load_yaml_from_docstring( cls_method.__doc__) if docstring_yaml: defaults.update(docstring_yaml) result[method_name] = defaults return result
def wrapped(func): self.spec.path(path, description=func.__doc__.partition('\n')[0], operations=load_yaml_from_docstring(func.__doc__)) return func
def operation_helper(spec, path, operations, **kwargs): """operations helper that allows passing an annotated function.""" view = kwargs.pop('view') dict_of_missing = temp_dict_missing(DictDefaultWrap()) hierarchy_docs = [] hierarchy_func = view while hierarchy_func: doc = getattr(hierarchy_func, '__orig_doc__', None) if doc: doc_yaml = yaml_utils.load_yaml_from_docstring(doc) if doc_yaml: hierarchy_docs.append(doc_yaml) hierarchy_func = getattr(hierarchy_func, '__wrapped__', None) def parameters_merging(o_parameters, d_parameters, yaml_doc): o_parameter_ids = {} for parameter in o_parameters: try: o_parameter_ids[(parameter['in'], parameter['name'])] = parameter except KeyError as e: raise IncompleteParameterSetting( "While solving parameter {}, with yaml document {}, related to view with doc {}," "the parameter attribute '{}' was missing".format(parameter, yaml_doc, view, e.args[0])) for parameter in d_parameters['parameters']: try: param_in = parameter['in'] param_name = parameter['name'] except KeyError as e: raise IncompleteParameterSetting( "While solving parameter {}, with yaml document {}, related to view with doc {}," "the parameter attribute '{}' was missing".format(parameter, yaml_doc, view, e.args[0])) o_parameter = o_parameter_ids.pop((param_in, param_name), None) if not o_parameter: o_parameter = {} o_parameters.append(o_parameter) def merge_with_default(original, defaults, yaml_doc=None): for operation_name, d_operation_content in defaults.items(): o_operation_content = original.setdefault(operation_name, d_operation_content) if o_operation_content is d_operation_content: if isinstance(o_operation_content, str): # This allows making placeholders for the string that were never set and not raising as a result. with dict_of_missing(original) as defaulted_dict: original[operation_name] = o_operation_content.format(f=defaulted_dict) continue if isinstance(o_operation_content, dict): merge_with_default(o_operation_content, d_operation_content, yaml_doc) elif isinstance(o_operation_content, list): if operation_name == "parameters": parameters_merging(o_operation_content, d_operation_content, yaml_doc) elif operation_name == 'security': o_security_methods = {tuple(security_rule.keys()) for security_rule in o_operation_content} new_auth_methods = [security_rule for security_rule in d_operation_content if tuple(security_rule.keys()) not in o_security_methods] o_operation_content.extend(new_auth_methods) pass elif operation_name in ('produces', 'consumes', 'tags'): o_operation_content[:] = list(OrderedDict.fromkeys( itertools.chain(o_operation_content, d_operation_content)).keys()) fallover_defaults = [] for hierarchy_doc in hierarchy_docs: try: fallover_defaults.append(hierarchy_doc.pop('_')) except KeyError: pass merge_with_default(operations, hierarchy_doc, view.__doc__) if fallover_defaults: underscore_default = fallover_defaults.pop(0) # Algorithmically, it is faster O(_n) if I merge all operations for all methods first than if I # run the same defaults sequence for all methods for default in fallover_defaults: merge_with_default(underscore_default, default, view.__doc__) for method, method_operations in operations.items(): merge_with_default(method_operations, underscore_default, view.__doc__) return path