Пример #1
0
    def map_serializer(self, auto_schema, method: str):
        """ custom handling for @extend_schema's injection of PolymorphicProxySerializer """
        serializer = self.target
        sub_components = []

        for sub_serializer in serializer.serializers:
            sub_serializer = force_instance(sub_serializer)
            resolved_sub_serializer = auto_schema.resolve_serializer(
                method, sub_serializer)

            try:
                discriminator_field = sub_serializer.fields[
                    serializer.resource_type_field_name]
                resource_type = discriminator_field.to_representation(None)
            except:  # noqa: E722
                warn(
                    f'sub-serializer {resolved_sub_serializer.name} of {serializer.component_name} '
                    f'must contain the discriminator field "{serializer.resource_type_field_name}". '
                    f'defaulting to sub-serializer name, but schema will likely not match the API.'
                )
                resource_type = resolved_sub_serializer.name

            sub_components.append((resource_type, resolved_sub_serializer.ref))

        return {
            'oneOf': [ref for _, ref in sub_components],
            'discriminator': {
                'propertyName': serializer.resource_type_field_name,
                'mapping': {
                    resource_type: ref['$ref']
                    for resource_type, ref in sub_components
                }
            }
        }
Пример #2
0
    def map_serializer(self, auto_schema, direction):
        sub_components = []
        serializer = self.target

        for sub_model in serializer.model_serializer_mapping:
            sub_serializer = serializer._get_serializer_from_model_or_instance(
                sub_model)
            resource_type = serializer.to_resource_type(sub_model)
            ref = auto_schema.resolve_serializer(sub_serializer, direction).ref
            sub_components.append((resource_type, ref))

            if not resource_type:
                warn(
                    f'discriminator mapping key is empty for {sub_serializer.__class__}. '
                    f'this might lead to code generation issues.')

        return {
            'oneOf': [ref for _, ref in sub_components],
            'discriminator': {
                'propertyName': serializer.resource_type_field_name,
                'mapping': {
                    resource_type: ref['$ref']
                    for resource_type, ref in sub_components
                },
            }
        }
Пример #3
0
    def _get_response_bodies(self):
        response_serializers = self.get_response_serializers()

        if is_serializer(response_serializers) or is_basic_type(response_serializers):
            if self.method == 'DELETE':
                return {'204': {'description': _('No response body')}}
            if self.method == 'POST' and getattr(self.view, 'action', None) == 'create':
                return {'201': self._get_response_for_code(response_serializers, '201')}
            return {'200': self._get_response_for_code(response_serializers, '200')}
        elif isinstance(response_serializers, dict):
            # custom handling for overriding default return codes with @extend_schema
            responses = {}
            for code, serializer in response_serializers.items():
                if isinstance(code, tuple):
                    code, media_types = str(code[0]), code[1:]
                else:
                    code, media_types = str(code), None
                content_response = self._get_response_for_code(serializer, code, media_types)
                if code in responses:
                    responses[code]['content'].update(content_response['content'])
                else:
                    responses[code] = content_response
            return responses
        else:
            warn(
                f'could not resolve "{response_serializers}" for {self.method} {self.path}. '
                f'Expected either a serializer or some supported override mechanism. '
                f'defaulting to generic free-form object.'
            )
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = _('Unspecified response body')
            return {'200': self._get_response_for_code(schema, '200')}
Пример #4
0
    def create_view(self, callback, method, request=None):
        """
        customized create_view which is called when all routes are traversed. part of this
        is instatiating views with default params. in case of custom routes (@action) the
        custom AutoSchema is injected properly through 'initkwargs' on view. However, when
        decorating plain views like retrieve, this initialization logic is not running.
        Therefore forcefully set the schema if @extend_schema decorator was used.
        """
        view = super().create_view(callback, method, request)

        if isinstance(view, viewsets.GenericViewSet) or isinstance(
                view, viewsets.ViewSet):
            action = getattr(view, view.action)
        elif isinstance(view, views.APIView):
            action = getattr(view, method.lower())
        else:
            warn(
                'Using not supported View class. Class must be derived from APIView '
                'or any of its subclasses like GenericApiView, GenericViewSet.'
            )
            return view

        if hasattr(action, 'kwargs') and 'schema' in action.kwargs:
            # might already be properly set in case of @action but overwrite for all cases
            view.schema = action.kwargs['schema']

        return view
Пример #5
0
    def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
        if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin):
            warn(
                'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you '
                'can simply restore this to the original class, extensions will take care of the '
                'rest.')

        model = get_view_model(auto_schema.view)
        if not model:
            return []

        filterset_class = self.target.get_filterset_class(
            auto_schema.view, model.objects.none())
        if not filterset_class:
            return []

        parameters = []
        for field_name, field in filterset_class.base_filters.items():
            schema, description, enum = (self.resolve_filter_field(
                auto_schema, model, filterset_class, field))
            parameters.append(
                build_parameter_type(
                    name=field_name,
                    required=field.extra['required'],
                    location=OpenApiParameter.QUERY,
                    description=description,
                    schema=schema,
                    enum=enum,
                ))

        return parameters
Пример #6
0
    def _get_serializer_field_meta(self, field):
        if not isinstance(field, serializers.Field):
            warn(
                f'unable to extract field metadata from field "{field}" because it appears '
                f'to be neither a Field nor a Serializer. Proper handling may require an '
                f'Extension or @extend_serializer_field. Skipping metadata extraction. '
            )
            return {}

        meta = {}
        if field.read_only:
            meta['readOnly'] = True
        if field.write_only:
            meta['writeOnly'] = True
        if field.allow_null:
            meta['nullable'] = True
        if field.default is not None and field.default != empty and not callable(
                field.default):
            default = field.to_representation(field.default)
            if isinstance(default, set):
                default = list(default)
            meta['default'] = default
        if field.help_text:
            meta['description'] = str(field.help_text)
        return meta
Пример #7
0
    def parse(self, request, public):
        """ Iterate endpoints generating per method path operations. """
        result = {}
        self._initialise_endpoints()

        for path, path_regex, method, view in self._get_paths_and_endpoints(
                None if public else request):
            if not self.has_view_permissions(path, method, view):
                continue

            # mocked request to allow certain operations in get_queryset and get_serializer[_class]
            # without exceptions being raised due to no request.
            if not request:
                request = spectacular_settings.GET_MOCK_REQUEST(
                    method, path, view, request)

            view.request = request

            if view.versioning_class and not is_versioning_supported(
                    view.versioning_class):
                warn(
                    f'using unsupported versioning class "{view.versioning_class}". view will be '
                    f'processed as unversioned view.')
            elif view.versioning_class:
                version = (
                    self.api_version  # generator was explicitly versioned
                    or getattr(request, 'version',
                               None)  # incoming request was versioned
                    or view.versioning_class.default_version  # fallback
                )
                if not version:
                    continue
                path = modify_for_versioning(self.inspector.patterns, method,
                                             path, view, version)
                if not operation_matches_version(view, version):
                    continue

            assert isinstance(view.schema, AutoSchema), (
                'Incompatible AutoSchema used on View. Is DRF\'s DEFAULT_SCHEMA_CLASS '
                'pointing to "drf_spectacular.openapi.AutoSchema" or any other drf-spectacular '
                'compatible AutoSchema?')
            operation = view.schema.get_operation(path, path_regex, method,
                                                  self.registry)

            # operation was manually removed via @extend_schema
            if not operation:
                continue

            # Normalise path for any provided mount url.
            if path.startswith('/'):
                path = path[1:]
            path = urljoin(self.url or '/', path)

            if spectacular_settings.CAMELIZE_NAMES:
                path, operation = camelize_operation(path, operation)

            result.setdefault(path, {})
            result[path][method.lower()] = operation

        return result
Пример #8
0
    def get_auth(self, path, method):
        """
        Obtains authentication classes and permissions from view. If authentication
        is known, resolve security requirement for endpoint and security definition for
        the component section.
        For custom authentication subclass ``OpenApiAuthenticationExtension``.
        """
        auths = []

        for authenticator in self.view.get_authenticators():
            scheme = OpenApiAuthenticationExtension.get_match(authenticator)
            if not scheme:
                warn(
                    f'could not resolve authenticator {authenticator.__class__}. There '
                    f'was no OpenApiAuthenticationExtension registered for that class. '
                    f'Try creating one by subclassing it. Ignoring for now.')
                continue

            auths.append(scheme.get_security_requirement(self.view))
            component = ResolvedComponent(
                name=scheme.name,
                type=ResolvedComponent.SECURITY_SCHEMA,
                object=authenticator.__class__,
                schema=scheme.get_security_definition(self.view))
            if component not in self.registry:
                self.registry.register(component)

        perms = [p.__class__ for p in self.view.get_permissions()]
        if permissions.AllowAny in perms:
            auths.append({})
        elif permissions.IsAuthenticatedOrReadOnly in perms and method not in (
                'PUT', 'PATCH', 'POST'):
            auths.append({})
        return auths
Пример #9
0
    def _get_response_bodies(self, path, method):
        response_serializers = self.get_response_serializers(path, method)

        if is_serializer(response_serializers) or is_basic_type(
                response_serializers):
            if method == 'DELETE':
                return {'204': {'description': 'No response body'}}
            return {
                '200':
                self._get_response_for_code(path, method, response_serializers)
            }
        elif isinstance(response_serializers, dict):
            # custom handling for overriding default return codes with @extend_schema
            return {
                str(code): self._get_response_for_code(path, method,
                                                       serializer)
                for code, serializer in response_serializers.items()
            }
        else:
            warn(
                f'could not resolve "{response_serializers}" for {method} {path}. '
                f'Expected either a serializer or some supported override mechanism. '
                f'defaulting to generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = 'Unspecified response body'
            return {'200': self._get_response_for_code(path, method, schema)}
Пример #10
0
 def _process_override_parameters(self):
     result = []
     for parameter in self.get_override_parameters():
         if isinstance(parameter, OpenApiParameter):
             if is_basic_type(parameter.type):
                 schema = build_basic_type(parameter.type)
             elif is_serializer(parameter.type):
                 schema = self.resolve_serializer(parameter.type, direction=None).ref
             else:
                 schema = parameter.type
             result.append(build_parameter_type(
                 name=parameter.name,
                 schema=schema,
                 location=parameter.location,
                 required=parameter.required,
                 description=parameter.description,
                 enum=parameter.enum,
                 deprecated=parameter.deprecated,
             ))
         elif is_serializer(parameter):
             # explode serializer into separate parameters. defaults to QUERY location
             mapped = self._map_serializer(parameter, direction=None)
             for property_name, property_schema in mapped['properties'].items():
                 result.append(build_parameter_type(
                     name=property_name,
                     schema=property_schema,
                     location=OpenApiParameter.QUERY,
                     required=property_name in mapped.get('required', [])
                 ))
         else:
             warn(f'could not resolve parameter annotation {parameter}. skipping.')
     return result
Пример #11
0
    def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
        if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin):
            warn(
                'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you '
                'can simply restore this to the original class, extensions will take care of the '
                'rest.')

        model = get_view_model(auto_schema.view)
        if not model:
            return []

        filterset_class = self.target.get_filterset_class(
            auto_schema.view, model.objects.none())
        if not filterset_class:
            return []

        parameters = []
        for field_name, field in filterset_class.base_filters.items():
            path = field.field_name.split('__')
            model_field = follow_field_source(model, path)

            parameters.append(
                build_parameter_type(
                    name=field_name,
                    required=field.extra['required'],
                    location=OpenApiParameter.QUERY,
                    description=field.label
                    if field.label is not None else field_name,
                    schema=auto_schema._map_model_field(model_field,
                                                        direction=None),
                    enum=[c for c, _ in field.extra.get('choices', [])],
                ))

        return parameters
Пример #12
0
    def _get_response_for_code(self, serializer):
        serializer = force_instance(serializer)

        if not serializer:
            return {'description': _('No response body')}
        elif isinstance(serializer, serializers.ListSerializer):
            if is_serializer(serializer.child):
                schema = self.resolve_serializer(serializer.child,
                                                 'response').ref
            else:
                schema = self._map_serializer_field(serializer.child,
                                                    'response')
        elif is_serializer(serializer):
            component = self.resolve_serializer(serializer, 'response')
            if not component.schema:
                return {'description': _('No response body')}
            schema = component.ref
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
        elif isinstance(serializer, dict):
            # bypass processing and use given schema directly
            schema = serializer
        else:
            warn(
                f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either '
                f'a serializer or some supported override mechanism. defaulting to '
                f'generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = _('Unspecified response body')

        if self._is_list_view(serializer) and not get_override(
                serializer, 'many') is False:
            schema = build_array_type(schema)
            paginator = self._get_paginator()

            if paginator and is_serializer(serializer):
                paginated_name = f'Paginated{self._get_serializer_name(serializer, "response")}List'
                component = ResolvedComponent(
                    name=paginated_name,
                    type=ResolvedComponent.SCHEMA,
                    schema=paginator.get_paginated_response_schema(schema),
                    object=paginated_name,
                )
                self.registry.register(component)
                schema = component.ref
            elif paginator:
                schema = paginator.get_paginated_response_schema(schema)

        return {
            'content': {
                mt: {
                    'schema': schema
                }
                for mt in self.map_renderers('media_type')
            },
            # Description is required by spec, but descriptions for each response code don't really
            # fit into our model. Description is therefore put into the higher level slots.
            # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject
            'description': ''
        }
Пример #13
0
    def _get_request_body(self):
        # only unsafe methods can have a body
        if self.method not in ('PUT', 'PATCH', 'POST'):
            return None

        serializer = force_instance(self.get_request_serializer())

        request_body_required = False
        if is_list_serializer(serializer):
            if is_serializer(serializer.child):
                component = self.resolve_serializer(serializer.child,
                                                    'request')
                schema = build_array_type(component.ref)
            else:
                schema = build_array_type(
                    self._map_serializer_field(serializer.child, 'request'))
            request_body_required = True
        elif is_serializer(serializer):
            if self.method == 'PATCH':
                serializer.partial = True
            component = self.resolve_serializer(serializer, 'request')
            if not component.schema:
                # serializer is empty so skip content enumeration
                return None
            schema = component.ref
            # request body is only required if any required property is not read-only
            readonly_props = [
                p for p, s in component.schema.get('properties', {}).items()
                if s.get('readOnly')
            ]
            required_props = component.schema.get('required', [])
            request_body_required = any(req not in readonly_props
                                        for req in required_props)
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
            if not schema:
                return None
        else:
            warn(
                f'could not resolve request body for {self.method} {self.path}. defaulting to generic '
                'free-form object. (maybe annotate a Serializer class?)')
            schema = build_object_type(
                additionalProperties={},
                description='Unspecified request body',
            )

        request_body = {
            'content': {
                media_type: build_media_type_object(
                    schema,
                    self._get_examples(serializer, 'request', media_type))
                for media_type in self.map_parsers()
            }
        }

        if request_body_required:
            request_body['required'] = request_body_required

        return request_body
Пример #14
0
    def get_security_definition(self, auto_schema):
        from rest_framework_simplejwt.settings import api_settings
        from drf_spectacular.plumbing import warn

        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],
        }
Пример #15
0
    def _resolve_path_parameters(self, variables):
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        parameters = []

        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''

            resolved_parameter = resolve_regex_path_parameter(
                self.path_regex,
                variable,
                self.map_renderers('format'),
            )

            if resolved_parameter:
                schema = resolved_parameter['schema']
            elif not model:
                warn(
                    f'could not derive type of path parameter "{variable}" because because it '
                    f'is untyped and {self.view.__class__} has no queryset. consider adding a '
                    f'type to the path (e.g. <int:{variable}>) or annotating the parameter '
                    f'type with @extend_schema. defaulting to "string".')
            else:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    # strip irrelevant meta data
                    irrelevant_field_meta = [
                        'readOnly', 'writeOnly', 'nullable', 'default'
                    ]
                    schema = {
                        k: v
                        for k, v in schema.items()
                        if k not in irrelevant_field_meta
                    }
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append(
                build_parameter_type(name=variable,
                                     location=OpenApiParameter.PATH,
                                     description=description,
                                     schema=schema))

        return parameters
Пример #16
0
    def _map_response_type_hint(self, method):
        hint = get_override(method, 'field') or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint), 'response')

        try:
            return resolve_type_hint(hint)
        except UnableToProceedError:
            warn(
                f'unable to resolve type hint for function "{method.__name__}". consider '
                f'using a type hint or @extend_schema_field. defaulting to string.'
            )
            return build_basic_type(OpenApiTypes.STR)
Пример #17
0
 def _get_serializer(self, path, method):
     view = self.view
     try:
         # try to circumvent queryset issues with calling get_serializer. if view has NOT
         # overridden get_serializer, its safe to use get_serializer_class.
         if view.__class__.get_serializer == GenericAPIView.get_serializer:
             return view.get_serializer_class()()
         return view.get_serializer()
     except Exception as exc:
         warn(
             f'Exception raised while getting serializer from {view.__class__.__name__}. Hint: '
             f'Is get_serializer_class() returning None or is get_queryset() not working without '
             f'a request? Ignoring the view for now. (Exception: {exc})')
         return None
Пример #18
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer(
            ).build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and isinstance(field, serializers.PrimaryKeyRelatedField):
            # special case handling only for _resolve_path_parameters() where neither queryset nor
            # parent is set by build_field. patch in queryset as _map_serializer_field requires it
            if not field.queryset:
                field.queryset = model_field.related_model.objects.none()
            return self._map_serializer_field(field, direction)
        elif field and not anyisinstance(
                field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, 'JSONField') and isinstance(
                model_field, models.JSONField):
            # fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping
            return build_basic_type(OpenApiTypes.OBJECT)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping.get(
                internal_type)
            if not field_cls:
                warn(
                    f'model field "{model_field.get_internal_type()}" has no mapping in '
                    f'ModelSerializer. it may be a deprecated field. defaulting to "string"'
                )
                return build_basic_type(OpenApiTypes.STR)
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"')
            return build_basic_type(OpenApiTypes.STR)
Пример #19
0
    def _map_model_field(self, field):
        assert isinstance(field, models.Field)
        drf_mapping = serializers.ModelSerializer.serializer_field_mapping

        if field.__class__ in drf_mapping:
            # use DRF native field resolution - taken from ModelSerializer.get_fields()
            return self._map_serializer_field(drf_mapping[field.__class__]())
        elif isinstance(field, models.ForeignKey):
            return self._map_model_field(field.target_field)
        else:
            warn(
                f'could not resolve model field "{field}" due to missing mapping.'
                'either your field is custom and not based on a known subclasses '
                'or we missed something. let us know.'
            )
            return build_basic_type(OpenApiTypes.STR)
Пример #20
0
    def _get_response_for_code(self, path, method, serializer):
        serializer = force_instance(serializer)

        if not serializer:
            return {'description': 'No response body'}
        elif isinstance(serializer, serializers.ListSerializer):
            schema = self.resolve_serializer(method, serializer.child).ref
        elif is_serializer(serializer):
            component = self.resolve_serializer(method, serializer)
            if not component:
                return {'description': 'No response body'}
            schema = component.ref
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
        elif isinstance(serializer, dict):
            # bypass processing and use given schema directly
            schema = serializer
        else:
            warn(
                f'could not resolve "{serializer}" for {method} {path}. Expected either '
                f'a serializer or some supported override mechanism. defaulting to '
                f'generic free-form object.')
            schema = build_basic_type(OpenApiTypes.OBJECT)
            schema['description'] = 'Unspecified response body'

        if isinstance(serializer, serializers.ListSerializer) or is_list_view(
                path, method, self.view):
            # TODO i fear is_list_view is not covering all the cases
            schema = build_array_type(schema)
            paginator = self._get_paginator()
            if paginator:
                schema = paginator.get_paginated_response_schema(schema)

        return {
            'content': {
                mt: {
                    'schema': schema
                }
                for mt in self.map_renderers(path, method)
            },
            # Description is required by spec, but descriptions for each response code don't really
            # fit into our model. Description is therefore put into the higher level slots.
            # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#responseObject
            'description': ''
        }
Пример #21
0
    def _map_type_hint(self, method):
        hint = getattr(method, '_spectacular_annotation', None) or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint))
        elif is_basic_type(hint):
            return build_basic_type(hint)
        elif getattr(hint, '__origin__', None) is typing.Union:
            if type(None) == hint.__args__[1] and len(hint.__args__) == 2:
                schema = build_basic_type(hint.__args__[0])
                schema['nullable'] = True
                return schema
            else:
                warn(f'type hint {hint} not supported yet. defaulting to "string"')
                return build_basic_type(OpenApiTypes.STR)
        else:
            warn(f'type hint for function "{method.__name__}" is unknown. defaulting to string.')
            return build_basic_type(OpenApiTypes.STR)
Пример #22
0
    def _resolve_path_parameters(self, variables):
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)
        parameters = []

        for idx, variable in enumerate(variables):
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''
            required = True

            resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable)

            if resolved_parameter:
                schema, required = resolved_parameter['schema'], resolved_parameter['required']
            elif not model:
                warn(
                    f'could not derive type of path parameter "{variable}" because '
                    f'{self.view.__class__} has no queryset. consider annotating the '
                    f'parameter type with @extend_schema. defaulting to "string".'
                )
            else:
                try:
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    # strip irrelevant meta data
                    irrelevant_field_meta = ['readOnly', 'writeOnly', 'nullable', 'default']
                    schema = {k: v for k, v in schema.items() if k not in irrelevant_field_meta}
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append({
                "name": variable,
                "in": "path",
                "required": required,
                "description": description,
                'schema': schema,
            })

        return parameters
Пример #23
0
    def _get_request_body(self):
        # only unsafe methods can have a body
        if self.method not in ('PUT', 'PATCH', 'POST'):
            return None

        serializer = force_instance(self.get_request_serializer())

        request_body_required = False
        if isinstance(serializer, serializers.ListSerializer):
            component = self.resolve_serializer(serializer.child, 'request')
            schema = build_array_type(component.ref)
            request_body_required = True
        elif is_serializer(serializer):
            component = self.resolve_serializer(serializer, 'request')
            if not component:
                # serializer is empty so skip content enumeration
                return None
            schema = component.ref
            if component.schema.get('required', []):
                request_body_required = True
        elif is_basic_type(serializer):
            schema = build_basic_type(serializer)
            if not schema:
                return None
        else:
            warn(
                f'could not resolve request body for {self.method} {self.path}. defaulting to generic '
                'free-form object. (maybe annotate a Serializer class?)'
            )
            schema = {
                'type': 'object',
                'additionalProperties': {},  # https://github.com/swagger-api/swagger-codegen/issues/1318
                'description': 'Unspecified request body',
            }

        request_body = {
            'content': {
                request_media_types: {'schema': schema} for request_media_types in self.map_parsers()
            }
        }
        if request_body_required:
            request_body['required'] = request_body_required

        return request_body
Пример #24
0
    def parse(self, request, public):
        """ Iterate endpoints generating per method path operations. """
        result = {}
        self._initialise_endpoints()

        for path, path_regex, method, view in self._get_paths_and_endpoints(
                None if public else request):
            if not self.has_view_permissions(path, method, view):
                continue

            if view.versioning_class and not is_versioning_supported(
                    view.versioning_class):
                warn(
                    f'using unsupported versioning class "{view.versioning_class}". view will be '
                    f'processed as unversioned view.')
            elif view.versioning_class:
                version = (
                    self.api_version  # generator was explicitly versioned
                    or getattr(request, 'version',
                               None)  # incoming request was versioned
                    or view.versioning_class.default_version  # fallback
                )
                path = modify_for_versioning(self.inspector.patterns, method,
                                             path, view, version)
                if not version or not operation_matches_version(view, version):
                    continue

            # beware that every access to schema yields a fresh object (descriptor pattern)
            operation = view.schema.get_operation(path, path_regex, method,
                                                  self.registry)

            # operation was manually removed via @extend_schema
            if not operation:
                continue

            # Normalise path for any provided mount url.
            if path.startswith('/'):
                path = path[1:]
            path = urljoin(self.url or '/', path)

            result.setdefault(path, {})
            result[path][method.lower()] = operation

        return result
Пример #25
0
    def _resolve_path_parameters(self, variables):
        parameters = []
        for variable in variables:
            schema = build_basic_type(OpenApiTypes.STR)
            description = ''

            resolved_parameter = resolve_regex_path_parameter(
                self.path_regex,
                variable,
                self.map_renderers('format'),
            )

            if resolved_parameter:
                schema = resolved_parameter['schema']
            elif get_view_model(self.view) is None:
                warn(
                    f'could not derive type of path parameter "{variable}" because because it '
                    f'is untyped and obtaining queryset from {self.view.__class__} failed. '
                    f'consider adding a type to the path (e.g. <int:{variable}>) or annotating '
                    f'the parameter type with @extend_schema. defaulting to "string".'
                )
            else:
                try:
                    model = get_view_model(self.view)
                    model_field = model._meta.get_field(variable)
                    schema = self._map_model_field(model_field, direction=None)
                    if 'description' not in schema and model_field.primary_key:
                        description = get_pk_description(model, model_field)
                except django_exceptions.FieldDoesNotExist:
                    warn(
                        f'could not derive type of path parameter "{variable}" because '
                        f'model "{model}" did contain no such field. consider annotating '
                        f'parameter with @extend_schema. defaulting to "string".'
                    )

            parameters.append(
                build_parameter_type(name=variable,
                                     location=OpenApiParameter.PATH,
                                     description=description,
                                     schema=schema))

        return parameters
Пример #26
0
    def _process_override_parameters(self):
        result = {}
        for parameter in self.get_override_parameters():
            if isinstance(parameter, OpenApiParameter):
                if is_basic_type(parameter.type):
                    schema = build_basic_type(parameter.type)
                elif is_serializer(parameter.type):
                    schema = self.resolve_serializer(parameter.type, 'request').ref
                else:
                    schema = parameter.type

                if parameter.exclude:
                    result[parameter.name, parameter.location] = None
                else:
                    result[parameter.name, parameter.location] = build_parameter_type(
                        name=parameter.name,
                        schema=schema,
                        location=parameter.location,
                        required=parameter.required,
                        description=parameter.description,
                        enum=parameter.enum,
                        deprecated=parameter.deprecated,
                        style=parameter.style,
                        explode=parameter.explode,
                        default=parameter.default,
                        examples=build_examples_list(parameter.examples),
                    )
            elif is_serializer(parameter):
                # explode serializer into separate parameters. defaults to QUERY location
                mapped = self._map_serializer(parameter, 'request')
                for property_name, property_schema in mapped['properties'].items():
                    result[property_name, OpenApiParameter.QUERY] = build_parameter_type(
                        name=property_name,
                        schema=property_schema,
                        description=property_schema.pop('description', None),
                        location=OpenApiParameter.QUERY,
                        required=property_name in mapped.get('required', []),
                    )
            else:
                warn(f'could not resolve parameter annotation {parameter}. skipping.')
        return result
Пример #27
0
    def _map_response_type_hint(self, method):
        hint = get_override(method, 'field') or typing.get_type_hints(method).get('return')

        if is_serializer(hint) or is_field(hint):
            return self._map_serializer_field(force_instance(hint), 'response')
        elif is_basic_type(hint, allow_none=False):
            return build_basic_type(hint)
        elif getattr(hint, '__origin__', None) is typing.Union:
            if type(None) == hint.__args__[1] and len(hint.__args__) == 2:
                schema = build_basic_type(hint.__args__[0])
                schema['nullable'] = True
                return schema
            else:
                warn(f'type hint {hint} not supported yet. defaulting to "string"')
                return build_basic_type(OpenApiTypes.STR)
        else:
            warn(
                f'type hint for function "{method.__name__}" is unknown. consider using '
                f'a type hint or @extend_schema_field. defaulting to string.'
            )
            return build_basic_type(OpenApiTypes.STR)
Пример #28
0
    def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
        if issubclass(self.target_class, SpectacularDjangoFilterBackendMixin):
            warn(
                'DEPRECATED - Spectacular\'s DjangoFilterBackend is superseded by extension. you '
                'can simply restore this to the original class, extensions will take care of the '
                'rest.')

        model = get_view_model(auto_schema.view)
        if not model:
            return []

        filterset_class = self.target.get_filterset_class(
            auto_schema.view, model.objects.none())
        if not filterset_class:
            return []

        return [
            self.resolve_filter_field(auto_schema, model, filterset_class,
                                      field_name, filter_field) for field_name,
            filter_field in filterset_class.base_filters.items()
        ]
Пример #29
0
 def _get_serializer(self):
     view = self.view
     try:
         if isinstance(view, GenericAPIView):
             # try to circumvent queryset issues with calling get_serializer. if view has NOT
             # overridden get_serializer, its safe to use get_serializer_class.
             if view.__class__.get_serializer == GenericAPIView.get_serializer:
                 return view.get_serializer_class()()
             return view.get_serializer()
         elif isinstance(view, APIView):
             # APIView does not implement the required interface, but be lenient and make
             # good guesses before giving up and emitting a warning.
             if callable(getattr(view, 'get_serializer', None)):
                 return view.get_serializer()
             elif callable(getattr(view, 'get_serializer_class', None)):
                 return view.get_serializer_class()()
             elif hasattr(view, 'serializer_class'):
                 return view.serializer_class
             else:
                 warn(
                     f'Unable to guess serializer for {view.__class__.__name__}. This is graceful '
                     f'fallback handling for APIViews. Consider using GenericAPIView as view base '
                     f'class, if view is under your control. ignoring view for now. '
                 )
         else:
             warn('Encountered unknown view base class. please report this issue. ignoring for now')
     except Exception as exc:
         warn(
             f'Exception raised while getting serializer from {view.__class__.__name__}. Hint: '
             f'Is get_serializer_class() returning None or is get_queryset() not working without '
             f'a request? Ignoring the view for now. (Exception: {exc})'
         )
Пример #30
0
    def _map_model_field(self, model_field, direction):
        assert isinstance(model_field, models.Field)
        # to get a fully initialized serializer field we use DRF's own init logic
        try:
            field_cls, field_kwargs = serializers.ModelSerializer(
            ).build_field(
                field_name=model_field.name,
                info=get_field_info(model_field.model),
                model_class=model_field.model,
                nested_depth=0,
            )
            field = field_cls(**field_kwargs)
        except:  # noqa
            field = None

        # For some cases, the DRF init logic either breaks (custom field with internal type) or
        # the resulting field is underspecified with regards to the schema (ReadOnlyField).
        if field and not anyisinstance(
                field, [serializers.ReadOnlyField, serializers.ModelField]):
            return self._map_serializer_field(field, direction)
        elif isinstance(model_field, models.ForeignKey):
            return self._map_model_field(model_field.target_field, direction)
        elif hasattr(models, model_field.get_internal_type()):
            # be graceful when the model field is not explicitly mapped to a serializer
            internal_type = getattr(models, model_field.get_internal_type())
            field_cls = serializers.ModelSerializer.serializer_field_mapping.get(
                internal_type)
            if not field_cls:
                warn(
                    f'model field "{model_field.get_internal_type()}" has no mapping in '
                    f'ModelSerializer. it may be a deprecated field. defaulting to "string"'
                )
                return build_basic_type(OpenApiTypes.STR)
            return self._map_serializer_field(field_cls(), direction)
        else:
            error(
                f'could not resolve model field "{model_field}". failed to resolve through '
                f'serializer_field_mapping, get_internal_type(), or any override mechanism. '
                f'defaulting to "string"')
            return build_basic_type(OpenApiTypes.STR)