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
Beispiel #4
0
 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
Beispiel #6
0
def test_load_yaml_from_docstring_empty_docstring(docstring):
    assert yaml_utils.load_yaml_from_docstring(docstring) == {}
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
    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
Beispiel #10
0
 def wrapped(func):
     self.spec.path(path,
                    description=func.__doc__.partition('\n')[0],
                    operations=load_yaml_from_docstring(func.__doc__))
     return func
Beispiel #11
0
    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