Example #1
0
def _verify_parameters(
    path: str,
    parameters: Sequence[OpenAPIParameter],
):
    """Verifies matching of parameters to the placeholders used in an URL-Template

    This works both ways, ensuring that no parameter is supplied which is then not used and that
    each template-variable in the URL-template has a corresponding parameter supplied,
    either globally or locally.

    Args:
        path:
            The URL-Template, for eample: '/user/{username}'

        parameters:
            A list of parameters. A parameter can either be a string referencing a
            globally defined parameter by name, or a dict containing a full parameter.

    Examples:

        In case of success, this function will return nothing.

          >>> _verify_parameters('/foo/{bar}', [{'name': 'bar', 'in': 'path'}])

        Yet, when problems are found, ValueErrors are raised.

          >>> _verify_parameters('/foo', [{'name': 'foo', 'in': 'path'}])
          Traceback (most recent call last):
          ...
          ValueError: Param 'foo', which is specified as 'path', not used in path. Found params: []

          >>> _verify_parameters('/foo/{bar}', [])
          Traceback (most recent call last):
          ...
          ValueError: Param 'bar', which is used in the HTTP path, was not specified in []

    Returns:
        Nothing.

    Raises:
        ValueError in case of a mismatch.

    """
    param_names = _names_of(parameters)
    path_params = path_parameters(path)
    for path_param in path_params:
        if path_param not in param_names:
            raise ValueError(
                f"Param {path_param!r}, which is used in the HTTP path, was not specified in "
                f"{parameters!r}")

    for param in parameters:
        if isinstance(
                param, dict
        ) and param['in'] == 'path' and param['name'] not in path_params:
            raise ValueError(
                f"Param {repr(param['name'])}, which is specified as 'path', not used in path. "
                f"Found params: {path_params}")

    find_all_parameters(parameters, errors='raise')
Example #2
0
    def __call__(self, func):
        """This is the real decorator.
        Returns:
        A wrapped function. The wrapper does input and output validation.
        """
        wrapped = wrap_with_validation(func, self.request_schema,
                                       self.response_schema)

        self.func = func
        self.operation_id = func.__module__ + "." + func.__name__

        ENDPOINT_REGISTRY.add_endpoint(
            self,
            find_all_parameters(_reduce_to_primitives(self.parameters)),
        )

        if not self.output_empty and self.response_schema is None:
            raise ValueError(
                f"{self.operation_id}: 'response_schema' required when output will be sent!"
            )

        if self.output_empty and self.response_schema:
            raise ValueError(
                f"{self.operation_id}: On empty output 'output_schema' may not be used."
            )

        return wrapped
Example #3
0
    def _add_api_spec(func):
        module_obj = import_string(func.__module__)
        module_name = module_obj.__name__
        operation_id = func.__module__ + "." + func.__name__

        ENDPOINT_REGISTRY.add_endpoint(
            module_name,
            name,
            method,
            path,
            find_all_parameters(primitive_parameters),
        )

        if not output_empty and response_schema is None:
            raise ValueError(f"{operation_id}: 'response_schema' required when "
                             f"output will be sent!")

        if output_empty and response_schema:
            raise ValueError(f"{operation_id}: On empty output 'output_schema' may not be used.")

        headers: Dict[str, PrimitiveParameter] = {}
        if etag in ('output', 'both'):
            headers.update(ETAG_HEADER_PARAM.header_dict())

        responses: ResponseType = {}

        # We don't(!) support any endpoint without an output schema.
        # Just define one!
        if response_schema is not None:
            responses['200'] = {
                'content': {
                    content_type: {
                        'schema': response_schema
                    },
                },
                'description': apispec.utils.dedent(response_schema.__doc__ or ''),
                'headers': headers,
            }

        if will_do_redirects:
            responses['302'] = {
                'description':
                    ('Either the resource has moved or has not yet completed. Please see this '
                     'resource for further information.')
            }

        # Actually, iff you don't want to give out anything, then we don't need a schema.
        if output_empty:
            responses['204'] = {
                'description': 'Operation done successfully. No further output.',
                'headers': headers,
            }

        tag_obj: OpenAPITag = {
            'name': module_name,
        }
        docstring_name = _docstring_name(module_obj.__doc__)
        if docstring_name:
            tag_obj['x-displayName'] = docstring_name
        docstring_desc = _docstring_description(module_obj.__doc__)
        if docstring_desc:
            tag_obj['description'] = docstring_desc
        _add_tag(tag_obj, tag_group='Endpoints')

        operation_spec: OperationSpecType = {
            'operationId': operation_id,
            'tags': [module_name],
            'description': '',
            'responses': {
                'default': {
                    'description': 'Any unsuccessful or unexpected result.',
                    'content': {
                        'application/problem+json': {
                            'schema': error_schema,
                        }
                    }
                }
            },
            'parameters': primitive_parameters,
        }

        if etag in ('input', 'both'):
            operation_spec['parameters'].append(ETAG_IF_MATCH_HEADER.to_dict())

        operation_spec['responses'].update(responses)

        if request_schema is not None:
            tag = _tag_from_schema(request_schema)
            _add_tag(tag, tag_group='Request Schemas')

            operation_spec['requestBody'] = {
                'required': request_body_required,
                'content': {
                    'application/json': {
                        'schema': request_schema,
                    }
                }
            }

        operation_spec['x-codeSamples'] = code_samples(path, method, request_schema, operation_spec)

        # If we don't have any parameters we remove the empty list, so the spec will not have it.
        if not operation_spec['parameters']:
            del operation_spec['parameters']

        docstring_name = _docstring_name(func.__doc__)
        if docstring_name:
            operation_spec['summary'] = docstring_name
        docstring_desc = _docstring_description(func.__doc__)
        if docstring_desc:
            operation_spec['description'] = docstring_desc

        apispec.utils.deepupdate(operation_spec, options)

        if func not in _SEEN_ENDPOINTS:
            # NOTE:
            # Only add the endpoint to the spec if not already done. We don't want
            # to add it multiple times. While this shouldn't be happening, doctest
            # sometimes complains that it would be happening. Not sure why. Anyways,
            # it's not a big deal as long as the SPEC is written correctly.
            SPEC.path(path=path, operations={method.lower(): operation_spec})
            _SEEN_ENDPOINTS.add(func)

        return wrap_with_validation(func, request_schema, response_schema)