Пример #1
0
def _signature_set_oas_gone(
    headers_arg: t.Any,
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    logger.debug('"headers" found in signature but not in operation')

    errors: t.Set[exceptions.Error] = set()
    param_mapping: t.Dict[model.OASParam, model.FunctionArgName] = {}

    try:
        r_headers_arg = types.resolve_root_type(headers_arg)
        entries = t.get_type_hints(r_headers_arg).items()

        logger.debug(
            'headers_arg is {h}:{t} with entries {e}',
            h=r_headers_arg,
            e=entries,
            t=type(r_headers_arg),
        )

        for hdr_param_name, hdr_param_type in entries:
            if hdr_param_name not in RESERVED_HEADERS:
                logger.error(
                    '{sig_key} is not one of {reserved_headers} headers',
                    sig_key=hdr_param_name,
                    reserved_headers=oas.OASReservedHeaders,
                )
                errors.add(
                    exceptions.Error(
                        param_name=f'headers.{hdr_param_name}',
                        reason='unknown',
                    ),
                )
            elif hdr_param_type != str:
                errors.add(
                    exceptions.Error(
                        param_name=f'headers.{hdr_param_name}',
                        reason=exceptions.IncorrectTypeReason(
                            actual=hdr_param_type,
                            expected=[str],
                        ),
                    ),
                )
            else:
                param_key = model.get_f_param(hdr_param_name)
                param_mapping[model.OASParam(
                    param_in='header',
                    param_name=RESERVED_HEADERS[param_key],
                )] = param_key
    except TypeError:
        # deal with mapping: in that case user will receive all
        # reserved headers inside of the handler
        for hdr_f_name, hdr_name in RESERVED_HEADERS.items():
            param_mapping[model.OASParam(
                param_in='header',
                param_name=hdr_name.lower(),
            )] = hdr_f_name

    return errors, param_mapping
Пример #2
0
def analyze(
    operation: oas.OASOperation,
    signature: t.Dict[str, t.Any],
) -> t.Set[exceptions.Error]:
    if 'return' not in signature:
        logger.error(
            'Operation {id} handler does not define return annotation',
            id=operation.id,
        )
        return {exceptions.Error(param_name='return', reason='missing')}
    else:
        return_type = signature.pop('return')
        rt_entries = getattr(return_type, '__annotations__', {}).copy()
        matching_keys = model.AXION_RESPONSE_KEYS.intersection(
            set(rt_entries.keys()))

        logger.opt(lazy=True).debug(
            'Operation {id} handler defines [{keys}] in return type',
            id=lambda: operation.id,
            keys=lambda: ','.join(rt_entries.keys()),
        )

        if matching_keys:
            return {
                *_analyze_http_code(
                    operation,
                    rt_entries.pop('http_code', None),
                ),
                *_analyze_cookies(
                    operation,
                    rt_entries.pop('cookies', None),
                ),
                *_analyze_headers(
                    operation,
                    rt_entries.pop('headers', None),
                ),
            }
        else:
            logger.opt(lazy=True).error(
                'Operation {id} handler return type is incorrect, '
                'expected {expected_type} but received {actual_type}',
                id=lambda: operation.id,
                expected_type=lambda: pipeline.Response,
                actual_type=lambda: return_type,
            )
            return {
                exceptions.Error(
                    param_name='return',
                    reason=exceptions.IncorrectTypeReason(
                        expected=[pipeline.Response],
                        actual=return_type,
                    ),
                ),
            }
Пример #3
0
def analyze(
    parameters: t.Sequence[oas.OASParameter],
    signature: t.Dict[str, t.Type[t.Any]],
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    errors: t.Set[exceptions.Error] = set()
    param_mapping: t.Dict[model.OASParam, model.FunctionArgName] = {}

    for op_param in parameters:
        logger.debug('Analyzing parameter={p}', p=op_param)
        try:
            handler_param_name = model.get_f_param(op_param.name)

            handler_param_type = signature.pop(handler_param_name)
            op_param_type = model.convert_oas_param_to_ptype(op_param)

            logger.opt(lazy=True).trace(
                'parameter={p} => p_type={p_type} f_type={f_type}',
                p=lambda: op_param,
                p_type=lambda: op_param_type,
                f_type=lambda: handler_param_type,
            )

            if handler_param_type != op_param_type:
                errors.add(
                    exceptions.Error(
                        param_name=op_param.name,
                        reason=exceptions.IncorrectTypeReason(
                            actual=handler_param_type,
                            expected=[op_param_type],
                        ),
                    ), )
            else:
                key = model.OASParam(
                    param_in=oas.parameter_in(op_param),
                    param_name=op_param.name,
                )
                param_mapping[key] = handler_param_name
        except KeyError:
            errors.add(
                exceptions.Error(
                    param_name=op_param.name,
                    reason='missing',
                ), )

    return errors, param_mapping
Пример #4
0
def _signature_set_oas_gone(
    cookies_arg: t.Any,
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    logger.error('"cookies" found in signature but not in operation')
    return {
        exceptions.Error(
            param_name='cookies',
            reason='unexpected',
        ),
    }, {}
Пример #5
0
def _analyze_signature_gone_oas_set(
) -> t.Tuple[t.Set[exceptions.Error], bool]:
    logger.error(
        'Operation defines a request body, but it is not specified in '
        'handler signature', )
    return {
        exceptions.Error(
            param_name='body',
            reason='missing',
        ),
    }, True
Пример #6
0
def _analyze_signature_set_oas_gone(
) -> t.Tuple[t.Set[exceptions.Error], bool]:
    logger.error(
        'Operation does not define a request body, but it is '
        'specified in handler signature.', )
    return {
        exceptions.Error(
            param_name='body',
            reason='unexpected',
        ),
    }, False
Пример #7
0
def _analyze_signature_set_oas_set(
    request_body: oas.OASRequestBody,
    body_arg: t.Type[t.Any],
) -> t.Tuple[t.Set[exceptions.Error], bool]:
    logger.trace('Operation defines both request body and argument handler')
    is_required = request_body.required
    is_arg_required = not ti.is_optional_type(body_arg)

    if is_required and not is_arg_required:
        return {
            exceptions.Error(
                param_name='body',
                reason=exceptions.IncorrectTypeReason(
                    actual=body_arg,
                    expected=model.BODY_TYPES,
                ),
            ),
        }, True
    return set(), True
Пример #8
0
def _analyze_cookies(
    operation: oas.OASOperation,
    cookies: t.Optional[t.Type[t.Any]],
) -> t.Set[exceptions.Error]:
    if cookies is not None:
        if types.is_any_type(cookies):
            logger.warning(
                'Detected usage of "return.cookies" declared as typing.Any. '
                'axion will allow such declaration but be warned that '
                'you will loose all the help linters (like mypy) offer.', )
            return set()
        elif not types.is_dict_like(cookies):
            return {
                exceptions.Error(
                    param_name='return.cookies',
                    reason=exceptions.IncorrectTypeReason(
                        expected=model.COOKIES_HEADERS_TYPE,
                        actual=cookies,
                    ),
                ),
            }
    return set()
Пример #9
0
def _analyze_http_code(
    operation: oas.OASOperation,
    rt_http_code: t.Optional[t.Type[t.Any]],
) -> t.Set[exceptions.Error]:

    if rt_http_code is None:
        # if there's no http_code in return Response
        # this is permitted only if there's single response defined in
        # OAS responses. User needs to set it otherwise how can we tell if
        # everything is correct
        if len(operation.responses) != 1:
            logger.opt(lazy=True).error(
                'Operation {id} handler skips return.http_code but it is impossible '
                ' with {count_of_ops} responses due to ambiguity.',
                id=lambda: operation.id,
                count_of_ops=lambda: len(operation.responses),
            )
            return {
                exceptions.Error(
                    param_name='return.http_code',
                    reason='missing',
                )
            }
        return set()

    elif ti.is_literal_type(rt_http_code):
        # this is acceptable. Literals hold particular values inside of them
        # if user wants to have it that way -> go ahead.
        # axion however will not validate a specific values in Literal.
        # this is by design and due to:
        # - error responses that axion implements via exceptions
        literal_types = types.literal_types(rt_http_code)
        if not all(
                issubclass(lmt, model.HTTP_CODE_TYPE)
                for lmt in literal_types):
            return {
                exceptions.Error(
                    param_name='return.http_code',
                    reason=exceptions.CustomReason(
                        f'expected {repr(te.Literal)}[int]'),
                ),
            }
        return set()

    elif ti.is_new_type(rt_http_code):
        # not quite sure why user would like to alias that
        # but it is not a problem for axion as long `NewType` embedded type
        # is fine
        return _analyze_http_code(operation, rt_http_code.__supertype__)

    elif issubclass(rt_http_code, bool):
        # yeah, Python rocks -> bool is subclass of an int
        # not quite sure wh that happens, perhaps someone sometime
        # will answer that question
        return {
            exceptions.Error(
                param_name='return.http_code',
                reason=exceptions.IncorrectTypeReason(
                    expected=[model.HTTP_CODE_TYPE],
                    actual=bool,
                ),
            ),
        }
    else:

        try:
            assert issubclass(rt_http_code, model.HTTP_CODE_TYPE)
            return set()
        except (AssertionError, TypeError):
            ...
        return {
            exceptions.Error(
                param_name='return.http_code',
                reason=exceptions.IncorrectTypeReason(
                    actual=rt_http_code,
                    expected=[
                        type(None),
                        model.HTTP_CODE_TYPE,
                        t.NewType('HttpCode', model.HTTP_CODE_TYPE),
                        te.Literal,
                    ],
                ),
            ),
        }
Пример #10
0
def analyze(
    handler: types.AnyCallable,
    operation: oas.OASOperation,
) -> model.AnalysisResult:
    logger.debug('Analyzing operation {id}', id=operation.id)
    signature = t.get_type_hints(handler)

    errors, has_body = body_arg.analyze(
        operation.request_body,
        signature.pop('body', None),
    )
    rt_errors = return_type.analyze(
        operation,
        signature,
    )

    errors.update(rt_errors)
    param_mapping: t.Dict[model.OASParam, model.FunctionArgName] = {}

    if operation.parameters:
        h_errors, h_params = headers_arg.analyze(
            oas.operation_filter_parameters(operation, 'header'),
            signature.pop('headers', None),
        )
        c_errors, c_params = cookies_arg.analyze(
            oas.operation_filter_parameters(operation, 'cookie'),
            signature.pop('cookies', None),
        )
        pq_errors, pq_params = path_query_arg.analyze(
            oas.operation_filter_parameters(operation, 'path', 'query'),
            signature,
        )

        if signature:
            logger.error(
                'Unconsumed arguments [{f_args}] detected in {op_id} handler signature',
                op_id=operation.id,
                f_args=', '.join(arg_key for arg_key in signature.keys()),
            )
            errors.update(
                exceptions.Error(
                    param_name=arg_key,
                    reason='unexpected',
                ) for arg_key in signature.keys())

        errors.update(pq_errors, h_errors, c_errors)
        param_mapping.update(pq_params)
        param_mapping.update(h_params)
        param_mapping.update(c_params)
    else:
        logger.debug('{id} does not declare any parameters', id=operation.id)

    if errors:
        logger.error(
            'Collected {count} mismatch error{s} for {op_id} handler',
            count=len(errors),
            op_id=operation.id,
            s='s' if len(errors) > 1 else '',
        )
        raise exceptions.InvalidHandlerError(
            operation_id=operation.id,
            errors=errors,
        )

    return model.AnalysisResult(
        param_mapping=param_mapping,
        has_body=has_body,
    )
Пример #11
0
def _analyze_headers_signature_set_oas_set(
    parameters: t.Sequence[oas.OASParameter],
    headers_arg: t.Any,
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    logger.debug('"headers" found both in signature and operation')

    errors: t.Set[exceptions.Error] = set()
    param_mapping: t.Dict[model.OASParam, model.FunctionArgName] = {}

    param_headers = {model.get_f_param(rh.name): str(rh.name) for rh in parameters}
    all_headers_names = {
        **param_headers,
        **RESERVED_HEADERS,
    }

    try:
        r_headers_arg = types.resolve_root_type(headers_arg)
        entries = t.get_type_hints(r_headers_arg).items()

        logger.debug(
            'headers_arg is {h}:{t} with entries {e}',
            h=r_headers_arg,
            t=type(r_headers_arg),
            e=entries,
        )

        for hdr_param_name, hdr_param_type in entries:
            if hdr_param_name in all_headers_names:
                # now tricky part, for reserved headers we enforce str
                # for oas headers we do type check
                if hdr_param_name in RESERVED_HEADERS and hdr_param_type != str:
                    errors.add(
                        exceptions.Error(
                            param_name=f'headers.{hdr_param_name}',
                            reason=exceptions.IncorrectTypeReason(
                                actual=hdr_param_type,
                                expected=[str],
                            ),
                        ),
                    )
                    continue
                elif hdr_param_name in param_headers:
                    oas_param = next(
                        filter(
                            lambda p: p.name == param_headers[model.get_f_param(
                                hdr_param_name,
                            )],
                            parameters,
                        ),
                    )
                    oas_param_type = model.convert_oas_param_to_ptype(oas_param)
                    if oas_param_type != hdr_param_type:
                        errors.add(
                            exceptions.Error(
                                param_name=f'headers.{hdr_param_name}',
                                reason=exceptions.IncorrectTypeReason(
                                    actual=hdr_param_type,
                                    expected=[str],
                                ),
                            ),
                        )
                        continue

                param_mapping[model.OASParam(
                    param_in='header',
                    param_name=all_headers_names[model.get_f_param(
                        hdr_param_name,
                    )].lower(),
                )] = model.get_f_param(hdr_param_name)

            else:
                errors.add(
                    exceptions.Error(
                        param_name=f'headers.{hdr_param_name}',
                        reason='unknown',
                    ),
                )
    except TypeError:
        for hdr_param_name, hdr_param_type in all_headers_names.items():
            param_mapping[model.OASParam(
                param_in='header',
                param_name=hdr_param_type.lower(),
            )] = hdr_param_name

    return errors, param_mapping
Пример #12
0
def analyze(
    parameters: t.Sequence[oas.OASParameter],
    headers_arg: t.Optional[t.Type[t.Any]],
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    """Analyzes signature of the handler against the headers.

    axion supports defining headers in signature using:
    - typing_extensions.TypedDict
    - typing.Mapping
    - typing.Dict
    - Any other type is rejected with appropriate error.

    Also, when parsing the signature along with operation, following is taken
    into account:
    1. function does not have "headers" argument and there are no custom OAS headers
        - OK
    2. function does not have "headers" argument and there are custom OAS headers
        - Warning
        - If there are custom headers defined user ought to specify them
          in signature. There was a point to put them inside there after all.
          However they might be used by a middleware or something, not necessarily
          handler. The warning is the only reliable thing to say.
    3. function has "headers" argument and there no custom OAS headers ->
        - OK
        - User might want to get_repr a hold with headers like "Content-Type"
        - With Mapping all reserved headers go in
        - With TypedDict we must see if users wants one of reserved headers
          Only reserved headers are allowed to be requested for.
    4. function has "headers" argument and there are customer OAS headers
        - OK
        - With Mapping all reserved headers + OAS headers go in
        - With TypedDict allowed keys covers
            - one or more of reserved headers
            - all of OAS headers with appropriate types

    See link bellow for information on reserved header
    https://swagger.io/docs/specification/describing-parameters/#header-parameters
    """
    has_param_headers = len(parameters) > 0

    if headers_arg is not None:
        # pre-check type of headers param in signature
        # must be either TypedDict, Mapping or a subclass of those
        is_mapping, is_any = (
            types.is_dict_like(headers_arg),
            types.is_any_type(headers_arg),
        )
        if not (is_mapping or is_any):
            return {
                exceptions.Error(
                    param_name='headers',
                    reason=exceptions.IncorrectTypeReason(
                        actual=headers_arg,
                        expected=model.COOKIES_HEADERS_TYPE,
                    ),
                ),
            }, {}
        elif is_any:
            logger.warning(
                'Detected usage of "headers" declared as typing.Any. '
                'axion will allow such declaration but be warned that '
                'you will loose all the help linters (like mypy) offer.',
            )
        if has_param_headers:
            return _analyze_headers_signature_set_oas_set(
                parameters=parameters,
                headers_arg=headers_arg,
            )
        else:
            return _signature_set_oas_gone(headers_arg)
    elif has_param_headers:
        return _signature_gone_oas_set()
    else:
        return _signature_gone_oas_gone()
Пример #13
0
def analyze(
    parameters: t.Sequence[oas.OASParameter],
    cookies_arg: t.Optional[t.Type[t.Any]],
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    """Analyzes signature of the handler against the cookies.

    axion supports defining cookies in signature using:
    - typing_extensions.TypedDict
    - typing.Mapping
    - typing.Dict
    - Any other type is rejected with appropriate error.

    Also, when parsing the signature along with operation, following is taken
    into account:
    1. function does not have "cookies" argument and there are no custom OAS cookies
        - OK
    2. function has "cookies" argument and there no custom OAS cookies ->
        - Error
    3. function does not have "cookies" argument and there are custom OAS cookies
        - Warning
        - If there are custom cookies defined user ought to specify them
          in signature. There was a point to put them inside there after all.
          However they might be used by a middleware or something, not necessarily
          handler. The warning is the only reliable thing to say.
    4. function has "cookies" argument and there are customer OAS cookies
        - OK
        - With Mapping/Dict all parameters go as they are defined in operation
        - With TypedDict allowed keys are only those defined in operation
    """
    has_param_cookies = len(parameters) > 0

    if cookies_arg is not None:
        # pre-check type of headers param in signature
        # must be either TypedDict, Mapping, Dict or a subclass of those
        is_mapping, is_any = (
            types.is_dict_like(cookies_arg),
            types.is_any_type(cookies_arg),
        )
        if not (is_mapping or is_any):
            return {
                exceptions.Error(
                    param_name='cookies',
                    reason=exceptions.IncorrectTypeReason(
                        actual=cookies_arg,
                        expected=model.COOKIES_HEADERS_TYPE,
                    ),
                ),
            }, {}
        elif is_any:
            logger.warning(
                'Detected usage of "cookies" declared as typing.Any. '
                'axion will allow such declaration but be warned that '
                'you will loose all the help linters (like mypy) offer.',
            )
        if has_param_cookies:
            return _signature_set_oas_set(
                parameters=parameters,
                cookies_arg=cookies_arg,
            )
        else:
            return _signature_set_oas_gone(cookies_arg)
    elif has_param_cookies:
        return _signature_gone_oas_set()
    else:
        return _signature_gone_oas_gone()
Пример #14
0
def _signature_set_oas_set(
    parameters: t.Sequence[oas.OASParameter],
    cookies_arg: t.Any,
) -> t.Tuple[t.Set[exceptions.Error], model.ParamMapping]:
    logger.debug('"cookies" found both in signature and operation')

    errors: t.Set[exceptions.Error] = set()

    param_mapping: t.Dict[model.OASParam, model.FunctionArgName] = {}
    param_cookies = {model.get_f_param(rh.name): rh.name for rh in parameters}

    try:
        r_cookies_arg = types.resolve_root_type(cookies_arg)
        entries = t.get_type_hints(r_cookies_arg).items()

        logger.debug(
            'cookies_arg is {h}:{t} with entries {e}',
            h=r_cookies_arg,
            t=type(r_cookies_arg),
            e=entries,
        )

        for cookie_param_name, cookie_param_type in entries:
            if cookie_param_name in param_cookies:

                oas_param = next(
                    filter(
                        lambda p: p.name == param_cookies[model.get_f_param(
                            cookie_param_name,
                        )],
                        parameters,
                    ),
                )
                oas_param_type = model.convert_oas_param_to_ptype(oas_param)
                if oas_param_type != cookie_param_type:
                    errors.add(
                        exceptions.Error(
                            param_name=f'cookies.{cookie_param_name}',
                            reason=exceptions.IncorrectTypeReason(
                                actual=cookie_param_type,
                                expected=[oas_param_type],
                            ),
                        ),
                    )
                else:
                    param_mapping[model.OASParam(
                        param_in='cookie',
                        param_name=param_cookies[model.get_f_param(
                            cookie_param_name,
                        )],
                    )] = model.get_f_param(cookie_param_name)

            else:
                errors.add(
                    exceptions.Error(
                        param_name=f'cookies.{cookie_param_name}',
                        reason='unknown',
                    ),
                )
    except TypeError:
        for hdr_param_name, hdr_param_type in param_cookies.items():
            param_mapping[model.OASParam(
                param_in='cookie',
                param_name=hdr_param_type,
            )] = hdr_param_name

    return errors, param_mapping