Ejemplo n.º 1
0
def load_enum_name_overrides():
    overrides = {}
    for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():
        if isinstance(choices, str):
            choices = deep_import_string(choices)
        if not choices:
            warn(
                f'unable to load choice override for {name} from ENUM_NAME_OVERRIDES. '
                f'please check module path string.'
            )
            continue
        if inspect.isclass(choices) and issubclass(choices, Choices):
            choices = choices.choices
        if inspect.isclass(choices) and issubclass(choices, Enum):
            choices = [c.value for c in choices]
        if isinstance(choices, Iterable) and isinstance(choices[0], str):
            choices = [(c, c) for c in choices]
        overrides[list_hash(list(dict(choices).keys()))] = name

    if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides):
        error(
            'ENUM_NAME_OVERRIDES has duplication issues. encountered multiple names '
            'for the same choice set. enum naming might be unexpected.'
        )
    return overrides
    def get_security_definition(self, auto_schema):
        from rest_framework_simplejwt.settings import api_settings

        if len(api_settings.AUTH_HEADER_TYPES) > 1:
            warn(
                f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify '
                f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.'
            )
        header_name = getattr(api_settings, 'AUTH_HEADER_NAME', 'HTTP_AUTHORIZATION')

        if (
            api_settings.AUTH_HEADER_TYPES[0] == 'Bearer'
            and header_name == 'HTTP_AUTHORIZATION'
        ):
            return {
                'type': 'http',
                'scheme': 'bearer',
                'bearerFormat': "JWT",
            }
        else:
            if header_name.startswith('HTTP_'):
                header_name = header_name[5:]
            header_name = header_name.capitalize()
            return {
                'type': 'apiKey',
                'in': 'header',
                'name': header_name,
                'description': _(
                    'Token-based authentication with required prefix "%s"'
                ) % api_settings.AUTH_HEADER_TYPES[0]
            }
Ejemplo n.º 3
0
def load_enum_name_overrides():
    overrides = {}
    for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items():
        if isinstance(choices, str):
            choices = deep_import_string(choices)
        if not choices:
            warn(
                f'unable to load choice override for {name} from ENUM_NAME_OVERRIDES. '
                f'please check module path string.')
            continue
        if inspect.isclass(choices) and issubclass(choices, Choices):
            choices = choices.choices
        if inspect.isclass(choices) and issubclass(choices, Enum):
            choices = [c.value for c in choices]
        normalized_choices = []
        for choice in choices:
            if isinstance(choice, str):
                normalized_choices.append(
                    (choice, choice))  # simple choice list
            elif isinstance(choice[1], (list, tuple)):
                normalized_choices.extend(
                    choice[1])  # categorized nested choices
            else:
                normalized_choices.append(choice)  # normal 2-tuple form
        overrides[list_hash(list(dict(normalized_choices).keys()))] = name

    if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides):
        error(
            'ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names '
            'for the same choice set. Enum naming might be unexpected.')
    return overrides
Ejemplo n.º 4
0
 def register(self, component: ResolvedComponent):
     if component in self:
         warn(
             f'trying to re-register a {component.type} component with name '
             f'{self._components[component.key].name}. this might lead to '
             f'a incorrect schema. Look out for reused names')
     self._components[component.key] = component
Ejemplo n.º 5
0
 def _load_class(cls):
     try:
         cls.target_class = import_string(cls.target_class)
     except ImportError:
         installed_apps = apps.app_configs.keys()
         if any(cls.target_class.startswith(app + '.') for app in installed_apps):
             warn(
                 f'registered extensions {cls.__name__} for "{cls.target_class}" '
                 f'has an installed app but target class was not found.'
             )
         cls.target_class = None
    def get_security_definition(self, auto_schema):
        from rest_framework_simplejwt.settings import api_settings

        if len(api_settings.AUTH_HEADER_TYPES) > 1:
            warn(
                f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify '
                f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.')
        return {
            'type': 'http',
            'scheme': 'bearer',
            'bearerFormat': api_settings.AUTH_HEADER_TYPES[0],
        }
    def get_security_definition(self, auto_schema):
        from rest_framework_simplejwt.settings import api_settings

        if len(api_settings.AUTH_HEADER_TYPES) > 1:
            warn(
                f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify '
                f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.')

        return build_bearer_security_scheme_object(
            header_name=getattr(api_settings, 'AUTH_HEADER_NAME',
                                'HTTP_AUTHORIZATION'),
            token_prefix=api_settings.AUTH_HEADER_TYPES[0],
            bearer_format='JWT')
Ejemplo n.º 8
0
def build_basic_type(obj):
    """
    resolve either enum or actual type and yield schema template for modification
    """
    if obj in OPENAPI_TYPE_MAPPING:
        return dict(OPENAPI_TYPE_MAPPING[obj])
    elif obj in PYTHON_TYPE_MAPPING:
        return dict(OPENAPI_TYPE_MAPPING[PYTHON_TYPE_MAPPING[obj]])
    elif obj is None or type(obj) is None:
        return dict(OPENAPI_TYPE_MAPPING[OpenApiTypes.NONE])
    else:
        warn(f'could not resolve type for "{obj}". defaulting to "string"')
        return dict(OPENAPI_TYPE_MAPPING[OpenApiTypes.STR])
Ejemplo n.º 9
0
def sanitize_result_object(result):
    # warn about and resolve operationId collisions with suffixes
    operations = defaultdict(list)
    for path, methods in result['paths'].items():
        for method, operation in methods.items():
            operations[operation['operationId']].append((path, method))
    for operation_id, paths in operations.items():
        if len(paths) == 1:
            continue
        warn(f'operationId "{operation_id}" has collisions {paths}. resolving with numeral suffixes.')
        for idx, (path, method) in enumerate(sorted(paths)[1:], start=2):
            suffix = str(idx) if spectacular_settings.CAMELIZE_NAMES else f'_{idx}'
            result['paths'][path][method]['operationId'] += suffix

    return result
Ejemplo n.º 10
0
    def __contains__(self, component):
        if component.key not in self._components:
            return False

        query_obj = component.object
        registry_obj = self._components[component.key].object
        query_class = query_obj if inspect.isclass(query_obj) else query_obj.__class__
        registry_class = query_obj if inspect.isclass(registry_obj) else registry_obj.__class__

        if query_class != registry_class:
            warn(
                f'Encountered 2 components with identical names "{component.name}" and '
                f'different classes {query_class} and {registry_class}. This will very '
                f'likely result in an incorrect schema. Try renaming one.'
            )
        return True
Ejemplo n.º 11
0
def _follow_return_type(a_callable):
    target_type = get_type_hints(a_callable).get('return')
    if target_type is None:
        return target_type
    origin, args = _get_type_hint_origin(target_type)
    if origin is typing.Union:
        type_args = [arg for arg in args if arg is not type(None)]  # noqa: E721
        if len(type_args) > 1:
            warn(
                f'could not traverse Union type, because we don\'t know which type to choose '
                f'from {type_args}. Consider terminating "source" on a custom property '
                f'that indicates the expected Optional/Union type. Defaulting to "string"'
            )
            return target_type
        # Optional:
        return type_args[0]
    return target_type
Ejemplo n.º 12
0
def get_view_model(view):
    """
    obtain model from view via view's queryset. try safer view attribute first
    before going through get_queryset(), which may perform arbitrary operations.
    """
    model = getattr(getattr(view, 'queryset', None), 'model', None)

    if model is not None:
        return model

    try:
        return view.get_queryset().model
    except Exception as exc:
        warn(
            f'failed to obtain model through view\'s queryset due to raised exception. '
            f'prevent this either by setting "queryset = Model.objects.none()" on the view, '
            f'having an empty fallback in get_queryset() or by using @extend_schema. '
            f'(Exception: {exc})')
Ejemplo n.º 13
0
def custom_preprocessing_hook(endpoints: Any) -> Any:  # TODO: organize method, rename
    from sentry.apidocs.public_exclusion_list import (
        EXCLUDED_FROM_PUBLIC_ENDPOINTS,
        PUBLIC_ENDPOINTS_FROM_JSON,
    )

    registered_endpoints = PUBLIC_ENDPOINTS_FROM_JSON | EXCLUDED_FROM_PUBLIC_ENDPOINTS

    filtered = []
    for (path, path_regex, method, callback) in endpoints:
        view = f"{callback.__module__}.{callback.__name__}"

        if callback.view_class.public and callback.view_class.private:
            warn(
                "both `public` and `private` cannot be defined at the same time, "
                "please remove one of the attributes."
            )
        if callback.view_class.public:
            # endpoints that are documented via tooling
            if method in callback.view_class.public:
                # only pass declared public methods of the endpoint
                # to the rest of the OpenAPI build pipeline
                filtered.append((path, path_regex, method, callback))

        elif view in registered_endpoints:
            # don't error if endpoint is added to exclusion list
            pass

        elif callback.view_class.private:
            # if the endpoint is explicitly private, that's okay.
            pass
        else:
            # any new endpoint that isn't accounted for should recieve this error when building api docs
            warn(
                f"{view} {method} is unaccounted for. "
                "Either document the endpoint and define the `public` attribute on the endpoint "
                "with the public HTTP methods, "
                "or set the `private` attribute on the endpoint to `True`. "
                "See https://develop.sentry.dev/api/public/ for more info on "
                "making APIs public."
            )

    return filtered
Ejemplo n.º 14
0
def detype_pattern(pattern):
    """
    return an equivalent pattern that accepts arbitrary values for path parameters.
    de-typing the path will ease determining a matching route without having properly
    formatted dummy values for all path parameters.
    """
    if isinstance(pattern, URLResolver):
        return URLResolver(
            pattern=detype_pattern(pattern.pattern),
            urlconf_name=[detype_pattern(p) for p in pattern.url_patterns],
            default_kwargs=pattern.default_kwargs,
            app_name=pattern.app_name,
            namespace=pattern.namespace,
        )
    elif isinstance(pattern, URLPattern):
        return URLPattern(
            pattern=detype_pattern(pattern.pattern),
            callback=pattern.callback,
            default_args=pattern.default_args,
            name=pattern.name,
        )
    elif isinstance(pattern, RoutePattern):
        return RoutePattern(
            route=re.sub(r'<\w+:(\w+)>', r'<\1>', pattern._route),
            name=pattern.name,
            is_endpoint=pattern._is_endpoint
        )
    elif isinstance(pattern, RegexPattern):
        detyped_regex = pattern._regex
        for name, regex in analyze_named_regex_pattern(pattern._regex).items():
            detyped_regex = detyped_regex.replace(
                f'(?P<{name}>{regex})',
                f'(?P<{name}>[^/]+)',
            )
        return RegexPattern(
            regex=detyped_regex,
            name=pattern.name,
            is_endpoint=pattern._is_endpoint
        )
    else:
        warn(f'unexpected pattern "{pattern}" encountered while simplifying urlpatterns.')
        return pattern
Ejemplo n.º 15
0
def get_view_model(view, emit_warnings=True):
    """
    obtain model from view via view's queryset. try safer view attribute first
    before going through get_queryset(), which may perform arbitrary operations.
    """
    model = getattr(getattr(view, 'queryset', None), 'model', None)

    if model is not None:
        return model

    try:
        return view.get_queryset().model
    except Exception as exc:
        if emit_warnings:
            warn(
                f'Failed to obtain model through view\'s queryset due to raised exception. '
                f'Prevent this either by setting "queryset = Model.objects.none()" on the '
                f'view, checking for "getattr(self, "swagger_fake_view", False)" in '
                f'get_queryset() or by simply using @extend_schema. (Exception: {exc})'
            )
Ejemplo n.º 16
0
    def decorator(view):
        view_methods = {m.__name__: m for m in get_view_methods(view)}

        for method_name, method_decorator in kwargs.items():
            if method_name not in view_methods:
                warn(
                    f'@extend_schema_view argument "{method_name}" was not found on view '
                    f'{view.__name__}. method override for "{method_name}" will be ignored.'
                )
                continue

            method = view_methods[method_name]
            # the context of derived methods must not be altered, as it belongs to the other
            # class. create a new context via the wrapping_decorator so the schema can be safely
            # stored in the wrapped_method. methods belonging to the view can be safely altered.
            if method_name in view.__dict__:
                method_decorator(method)
            else:
                setattr(view, method_name, wrapping_decorator(method_decorator, method))
        return view
Ejemplo n.º 17
0
def follow_field_source(model, path):
    """
    a model traversal chain "foreignkey.foreignkey.value" can either end with an actual model field
    instance "value" or a model property function named "value". differentiate the cases.

    :return: models.Field or function object
    """
    try:
        return _follow_field_source(model, path)
    except UnableToProceedError as e:
        warn(e)
    except Exception as exc:
        warn(
            f'could not resolve field on model {model} with path "{".".join(path)}". '
            f'this is likely a custom field that does some unknown magic. maybe '
            f'consider annotating the field/property? defaulting to "string". (Exception: {exc})'
        )

    def dummy_property(obj) -> str:
        pass
    return dummy_property
    def resolve_filter_field(self, auto_schema, model, filterset_class,
                             field_name, filter_field):
        from django_filters.rest_framework import filters

        unambiguous_mapping = {
            filters.CharFilter: OpenApiTypes.STR,
            filters.BooleanFilter: OpenApiTypes.BOOL,
            filters.DateFilter: OpenApiTypes.DATE,
            filters.DateTimeFilter: OpenApiTypes.DATETIME,
            filters.IsoDateTimeFilter: OpenApiTypes.DATETIME,
            filters.TimeFilter: OpenApiTypes.TIME,
            filters.UUIDFilter: OpenApiTypes.UUID,
            filters.DurationFilter: OpenApiTypes.DURATION,
            filters.OrderingFilter: OpenApiTypes.STR,
            filters.TimeRangeFilter: OpenApiTypes.TIME,
            filters.DateFromToRangeFilter: OpenApiTypes.DATE,
            filters.IsoDateTimeFromToRangeFilter: OpenApiTypes.DATETIME,
            filters.DateTimeFromToRangeFilter: OpenApiTypes.DATETIME,
        }
        if isinstance(filter_field, tuple(unambiguous_mapping)):
            for cls in filter_field.__class__.__mro__:
                if cls in unambiguous_mapping:
                    schema = build_basic_type(unambiguous_mapping[cls])
                    break
        elif isinstance(filter_field,
                        (filters.NumberFilter, filters.NumericRangeFilter)):
            # NumberField is underspecified by itself. try to find the
            # type that makes the most sense or default to generic NUMBER
            if filter_field.method:
                schema = self._build_filter_method_type(
                    filterset_class, filter_field)
                if schema['type'] not in ['integer', 'number']:
                    schema = build_basic_type(OpenApiTypes.NUMBER)
            else:
                model_field = self._get_model_field(filter_field, model)
                if isinstance(model_field,
                              (models.IntegerField, models.AutoField)):
                    schema = build_basic_type(OpenApiTypes.INT)
                elif isinstance(model_field, models.FloatField):
                    schema = build_basic_type(OpenApiTypes.FLOAT)
                elif isinstance(model_field, models.DecimalField):
                    schema = build_basic_type(
                        OpenApiTypes.NUMBER)  # TODO may be improved
                else:
                    schema = build_basic_type(OpenApiTypes.NUMBER)
        elif filter_field.method:
            # try to make best effort on the given method
            schema = self._build_filter_method_type(filterset_class,
                                                    filter_field)
        else:
            # last resort is to lookup the type via the model field.
            model_field = self._get_model_field(filter_field, model)
            if isinstance(model_field, models.Field):
                try:
                    schema = auto_schema._map_model_field(model_field,
                                                          direction=None)
                except Exception as exc:
                    warn(
                        f'Exception raised while trying resolve model field for django-filter '
                        f'field "{field_name}". Defaulting to string (Exception: {exc})'
                    )
                    schema = build_basic_type(OpenApiTypes.STR)
            else:
                # default to string if nothing else works
                schema = build_basic_type(OpenApiTypes.STR)

        # primary keys are usually non-editable (readOnly=True) and map_model_field correctly
        # signals that attribute. however this does not apply in this context.
        schema.pop('readOnly', None)
        # enrich schema with additional info from filter_field
        enum = schema.pop('enum', None)
        if 'choices' in filter_field.extra:
            enum = [c for c, _ in filter_field.extra['choices']]
        if enum:
            schema['enum'] = sorted(enum, key=str)

        description = schema.pop('description', None)
        if filter_field.extra.get('help_text', None):
            description = filter_field.extra['help_text']
        elif filter_field.label is not None:
            description = filter_field.label

        # parameter style variations based on filter base class
        if isinstance(filter_field, filters.BaseCSVFilter):
            schema = build_array_type(schema)
            field_names = [field_name]
            explode = False
            style = 'form'
        elif isinstance(filter_field, filters.MultipleChoiceFilter):
            schema = build_array_type(schema)
            field_names = [field_name]
            explode = True
            style = 'form'
        elif isinstance(filter_field,
                        (filters.RangeFilter, filters.NumericRangeFilter)):
            try:
                suffixes = filter_field.field_class.widget.suffixes
            except AttributeError:
                suffixes = ['min', 'max']
            field_names = [
                f'{field_name}_{suffix}' if suffix else field_name
                for suffix in suffixes
            ]
            explode = None
            style = None
        else:
            field_names = [field_name]
            explode = None
            style = None

        return [
            build_parameter_type(name=field_name,
                                 required=filter_field.extra['required'],
                                 location=OpenApiParameter.QUERY,
                                 description=description,
                                 schema=schema,
                                 explode=explode,
                                 style=style) for field_name in field_names
        ]