def path_helper(self, operations, view, **kwargs): """Path helper that allows passing a Sanic view function.""" operations_from_docs = load_operations_from_docstring(view.__doc__) # Operation is specified in OpenAPI specifications # Refer: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operation-object if operations_from_docs: operations.update(operations_from_docs) for method in view.view_class.methods: method_name = method.lower() method = getattr(view.view_class, method_name) method_operation = load_yaml_from_docstring(method.__doc__) if method_operation: operations[method_name] = load_yaml_from_docstring( method.__doc__)
def get_operations(view, operations, autodoc=True): if operations is not None: return operations operations = {} # views can be class based if view.get("attr"): global_meta = load_operations_from_docstring(view["callable"].__doc__) if global_meta: operations.update(global_meta) f_view = getattr(view["callable"], view["attr"]) # or just function callables else: f_view = view.get("callable") methods = view.get("request_methods") view_operations = load_operations_from_docstring(f_view.__doc__) if not view_operations: view_operations = {} if is_string(methods): methods = [methods] if not methods: methods = ALL_METHODS[:] operation = load_yaml_from_docstring(f_view.__doc__) if operation: for method in methods: view_operations[method.lower()] = operation elif autodoc: for method in methods: view_operations.setdefault(method.lower(), {"responses": {}}) operations.update(view_operations) return operations
def _extensions_from_handler(handler_class): """Returns extensions dict from handler docstring :param handler_class: :type handler_class: RequestHandler descendant """ extensions = utils.load_yaml_from_docstring(handler_class.__doc__) or {} return extensions
def _operations_from_methods(handler_class): """Generator of operations described in handler's http methods :param handler_class: :type handler_class: RequestHandler descendant """ for httpmethod in utils.PATH_KEYS: method = getattr(handler_class, httpmethod) operation_data = utils.load_yaml_from_docstring(method.__doc__) if operation_data: operation = {httpmethod: operation_data} yield operation
def parse_operations(resource): """Parse operations for each method in a flask-restful resource""" operations = {} for method in resource.methods: docstring = getattr(resource, method.lower()).__doc__ if docstring: operation = utils.load_yaml_from_docstring(docstring) if not operation: logging.getLogger(__name__).warning( 'Cannot load docstring for {}/{}'.format(resource, method)) operations[method.lower()] = operation or dict() return operations
def test_load_yaml_from_docstring(): def f(): """ Foo bar baz quux --- herp: 1 derp: 2 """ result = utils.load_yaml_from_docstring(f.__doc__) assert result == {'herp': 1, 'derp': 2}
def update_operations_specs(cls, operations, methods, method=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(specs) defaults.setdefault('consumes', ['application/json']) defaults.setdefault('produces', ['application/json']) defaults.setdefault('tags', [cls.meta.name]) 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': {}}} }) if cls.Schema: defaults['responses'][200]['schema'] = {'$ref': '#/definitions/%s' % cls.meta.name} if method_name in ('put', 'patch', 'post'): defaults.setdefault('parameters', []) schema = {} if cls.Schema: schema['$ref'] = '#/definitions/%s' % cls.meta.name defaults['parameters'].append({ 'name': 'body', 'in': 'body', 'description': 'Resource Body', 'required': True, 'schema': schema, }) if method_name in operations: defaults.update(operations[method_name]) docstring_yaml = utils.load_yaml_from_docstring(cls_method.__doc__) if docstring_yaml: defaults.update(docstring_yaml) result[method_name] = defaults return result
def operations_from_methods(handler, extensions): """Generator of operations described in handler's http methods :param handler: """ for httpmethod in utils.PATH_KEYS: method = getattr(handler, httpmethod, None) if method is None: continue operation_data = dict() operation_data.update(extensions) operation_data.update(getattr(method, 'apispec', dict())) operation_data.update( utils.load_yaml_from_docstring(method.__doc__) or {}) if operation_data: operation = {httpmethod: operation_data} yield operation
def _add_schema_to_spec(self, schema): schema_name = schema.__name__ data = load_yaml_from_docstring(schema.__doc__) or {} missed = set(('name', 'description')) - data.keys() if missed: for attr in missed: data[attr] = schema_name missed = ', '.join(f'"{m}"' for m in missed) warnings.warn( f'For schema <{schema_name}> not provided {missed}.' ' Fail to default values.' ' Schema class name will be used insted.', RuntimeWarning) data['schema'] = schema self.spec_schemas.append(data)
def path_from_view(spec, view, **kwargs): """Path helper that allows passing a Flask view function.""" rule = _rule_for_view(view) path = flaskpath2swagger(rule.rule) app_root = current_app.config['APPLICATION_ROOT'] or '/' path = urljoin(app_root.rstrip('/') + '/', path.lstrip('/')) operations = utils.load_operations_from_docstring(view.__doc__) path = Path(path=path, operations=operations) if hasattr(view, 'view_class') and issubclass(view.view_class, MethodView): operations = {} for method in view.methods: method_name = method.lower() method = getattr(view.view_class, method_name) docstring_yaml = utils.load_yaml_from_docstring(method.__doc__) operations[method_name] = docstring_yaml or dict() path.operations.update(operations) return path
def path_helper(self, operations, view, **kwargs): """Path helper that allows passing a Flask view function.""" rule = self._rule_for_view(view) operations.update( utils.load_operations_from_docstring(view.__doc__) or {}) if hasattr(view, 'view_class') and issubclass(view.view_class, MethodView): for method in view.methods: if method in rule.methods: method_name = method.lower() method = getattr(view.view_class, method_name) docstring_yaml = utils.load_yaml_from_docstring( method.__doc__) operations[method_name] = docstring_yaml or dict() path = self.flaskpath2openapi(rule.rule) app_root = current_app.config['APPLICATION_ROOT'] or '/' return urljoin(app_root.rstrip('/') + '/', path.lstrip('/'))
def path_helper(self, operations, resource, **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(utils.load_operations_from_docstring(resource.__doc__) or {}) path = resource_uri_mapping[resource] for method in falcon.constants.HTTP_METHODS: http_verb = method.lower() method_name = "on_" + http_verb if hasattr(resource, method_name): method = getattr(resource, method_name) docstring_yaml = utils.load_yaml_from_docstring(method.__doc__) operations[http_verb] = docstring_yaml or dict() return path
def update_operations_specs(cls, operations, methods, method=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(specs) defaults.setdefault('consumes', ['application/json']) defaults.setdefault('produces', ['application/json']) defaults.setdefault('tags', [cls.meta.name]) 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': {} } } }) if cls.Schema: defaults['responses'][200]['schema'] = { '$ref': '#/definitions/%s' % cls.meta.name } if method_name in ('put', 'patch', 'post'): defaults.setdefault('parameters', []) schema = {} if cls.Schema: schema['$ref'] = '#/definitions/%s' % cls.meta.name defaults['parameters'].append({ 'name': 'body', 'in': 'body', 'description': 'Resource Body', 'required': True, 'schema': schema, }) if method_name in operations: defaults.update(operations[method_name]) docstring_yaml = utils.load_yaml_from_docstring(cls_method.__doc__) if docstring_yaml: defaults.update(docstring_yaml) result[method_name] = defaults return result
def extensions_from_handler(handler): """Returns extensions dict from handler docstring :param handler: """ extensions = utils.load_yaml_from_docstring(handler.__doc__) or {} return extensions
def add_pyramid_paths(spec, route_name, request=None, request_method=None, operations=None, **kwargs): """ Adds a route and view info to spec :param spec: ApiSpec object :param route_name: Route name to inspect :param request: Request object, if `None` then `get_current_request()` will be used :param request_method: Request method predicate :param operations: Operations dict that will be used instead of introspection :param kwargs: Additional kwargs for predicate matching :return: """ from pyramid.threadlocal import get_current_request if request is None: request = get_current_request() registry = request.registry introspector = registry.introspector route = introspector.get("routes", route_name) views = introspector.related(route) # needs to be rewritten to internal name if request_method: kwargs["request_methods"] = request_method # kwargs.setdefault('route_name', route_name) for view in views: matches = True for kw in kwargs.keys(): # request_methods can be either a list of strings or a string # so lets normalize via sets if kw == "request_methods": if is_string(kwargs[kw]): kwargs[kw] = [kwargs[kw]] methods = view.get(kw) or ALL_METHODS if is_string(methods): methods = [methods] if not set(kwargs[kw] or []).intersection(methods): matches = False else: if not view.get(kw) == kwargs[kw]: matches = False if not matches: continue final_operations = {} # views can be class based if view.get("attr"): global_meta = load_operations_from_docstring( view["callable"].__doc__) if global_meta: final_operations.update(global_meta) f_view = getattr(view["callable"], view["attr"]) # or just function callables else: f_view = view.get("callable") if operations is None: methods = view.get("request_methods") view_operations = load_operations_from_docstring(f_view.__doc__) if not view_operations: view_operations = {} if is_string(methods): methods = [methods] if not methods: methods = ALL_METHODS[:] operation = load_yaml_from_docstring(f_view.__doc__) if operation: for method in methods: view_operations[method.lower()] = operation final_operations.update(view_operations) else: final_operations = operations spec.add_path(route["pattern"], operations=final_operations)
def operations_merging(spec, path, operations, view, **kwargs): """operations helper that allows passing an annotated function.""" 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 = 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(path.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 path.operations.items(): merge_with_default(method_operations, underscore_default, view.__doc__) return path