This commit is contained in:
Iliyan Angelov
2025-11-24 03:52:08 +02:00
parent dfcaebaf8c
commit 366f28677a
18241 changed files with 865352 additions and 567 deletions

View File

@@ -0,0 +1,69 @@
from ..app_settings import swagger_settings
from .base import (
BaseInspector,
FieldInspector,
FilterInspector,
NotHandled,
PaginatorInspector,
SerializerInspector,
ViewInspector,
)
from .field import (
CamelCaseJSONFilter,
ChoiceFieldInspector,
DictFieldInspector,
FileFieldInspector,
HiddenFieldInspector,
InlineSerializerInspector,
JSONFieldInspector,
RecursiveFieldInspector,
ReferencingSerializerInspector,
RelatedFieldInspector,
SerializerMethodFieldInspector,
SimpleFieldInspector,
StringDefaultFieldInspector,
)
from .query import (
CoreAPICompatInspector,
DjangoRestResponsePagination,
DrfAPICompatInspector,
)
from .view import SwaggerAutoSchema
# these settings must be accessed only after defining/importing all the classes in this
# module to avoid ImportErrors
ViewInspector.field_inspectors = swagger_settings.DEFAULT_FIELD_INSPECTORS
ViewInspector.filter_inspectors = swagger_settings.DEFAULT_FILTER_INSPECTORS
ViewInspector.paginator_inspectors = swagger_settings.DEFAULT_PAGINATOR_INSPECTORS
__all__ = [
# base inspectors
"BaseInspector",
"FilterInspector",
"PaginatorInspector",
"FieldInspector",
"SerializerInspector",
"ViewInspector",
# filter and pagination inspectors
"DrfAPICompatInspector",
"CoreAPICompatInspector",
"DjangoRestResponsePagination",
# field inspectors
"InlineSerializerInspector",
"RecursiveFieldInspector",
"ReferencingSerializerInspector",
"RelatedFieldInspector",
"SimpleFieldInspector",
"FileFieldInspector",
"ChoiceFieldInspector",
"DictFieldInspector",
"JSONFieldInspector",
"StringDefaultFieldInspector",
"CamelCaseJSONFilter",
"HiddenFieldInspector",
"SerializerMethodFieldInspector",
# view inspectors
"SwaggerAutoSchema",
# module constants
"NotHandled",
]

View File

@@ -0,0 +1,590 @@
import inspect
import logging
from rest_framework import serializers
from .. import openapi
from ..utils import force_real_str, get_field_default, get_object_classes, is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to
# handle an object
NotHandled = object()
logger = logging.getLogger(__name__)
def is_callable_method(cls_or_instance, method_name):
method = getattr(cls_or_instance, method_name)
if inspect.ismethod(method) and getattr(method, "__self__", None):
# bound classmethod or instance method
return method, True
from inspect import getattr_static
return method, isinstance(
getattr_static(cls_or_instance, method_name, None), staticmethod
)
def call_view_method(view, method_name, fallback_attr=None, default=None):
"""Call a view method which might throw an exception. If an exception is thrown, log
an informative error message and return the value of fallback_attr, or default if
not present. The method must be callable without any arguments except cls or self.
:param view: view class or instance; if a class is passed, instance methods won't be
called
:type view: rest_framework.views.APIView or type[rest_framework.views.APIView]
:param str method_name: name of a method on the view
:param str fallback_attr: name of an attribute on the view to fall back on, if
calling the method fails
:param default: default value if all else fails
:return: view method's return value, or value of view's fallback_attr, or default
:rtype: any or None
"""
if hasattr(view, method_name):
try:
view_method, is_callabale = is_callable_method(view, method_name)
if is_callabale:
return view_method()
except Exception: # pragma: no cover
logger.warning(
"view's %s.%s raised exception during schema generation; use "
"`getattr(self, 'swagger_fake_view', False)` to detect and "
"short-circuit this",
type(view).__name__,
method_name,
exc_info=True,
)
if fallback_attr and hasattr(view, fallback_attr):
return getattr(view, fallback_attr)
return default
class BaseInspector:
def __init__(self, view, path, method, components, request):
"""
:param rest_framework.views.APIView view: the view associated with this endpoint
:param str path: the path component of the operation URL
:param str method: the http method of the operation
:param openapi.ReferenceResolver components: referenceable components
:param rest_framework.request.Request request: the request made against the
schema view; can be None
"""
self.view = view
self.path = path
self.method = method
self.components = components
self.request = request
def process_result(self, result, method_name, obj, **kwargs):
"""After an inspector handles an object (i.e. returns a value other than
:data:`.NotHandled`), all inspectors that were probed get the chance to alter
the result, in reverse order. The inspector that handled the object is the first
to receive a ``process_result`` call with the object it just returned.
This behavior is similar to the Django request/response middleware processing.
If this inspector has no post-processing to do, it should just ``return result``
(the default implementation).
:param result: the return value of the winning inspector, or ``None`` if no
inspector handled the object
:param str method_name: name of the method that was called on the inspector
:param obj: first argument passed to inspector method
:param kwargs: additional arguments passed to inspector method
:return:
"""
return result
def probe_inspectors(self, inspectors, method_name, obj, initkwargs=None, **kwargs):
"""Probe a list of inspectors with a given object. The first inspector in the
list to return a value that is not :data:`.NotHandled` wins.
:param list[type[BaseInspector]] inspectors: list of inspectors to probe
:param str method_name: name of the target method on the inspector
:param obj: first argument to inspector method
:param dict initkwargs: extra kwargs for instantiating inspector class
:param kwargs: additional arguments to inspector method
:return: the return value of the winning inspector, or ``None`` if no inspector
handled the object
"""
initkwargs = initkwargs or {}
tried_inspectors = []
for inspector in inspectors:
assert inspect.isclass(inspector), (
"inspector must be a class, not an object"
)
assert issubclass(inspector, BaseInspector), (
"inspectors must subclass BaseInspector"
)
inspector = inspector(
self.view,
self.path,
self.method,
self.components,
self.request,
**initkwargs,
)
tried_inspectors.append(inspector)
method = getattr(inspector, method_name, None)
if method is None:
continue
result = method(obj, **kwargs)
if result is not NotHandled:
break
else: # pragma: no cover
logger.warning(
"%s ignored because no inspector in %s handled it (operation: %s)",
obj,
inspectors,
method_name,
)
result = None
for inspector in reversed(tried_inspectors):
result = inspector.process_result(result, method_name, obj, **kwargs)
return result
def get_renderer_classes(self):
"""Get the renderer classes of this view by calling `get_renderers`.
:return: renderer classes
:rtype: list[type[rest_framework.renderers.BaseRenderer]]
"""
return get_object_classes(
call_view_method(self.view, "get_renderers", "renderer_classes", [])
)
def get_parser_classes(self):
"""Get the parser classes of this view by calling `get_parsers`.
:return: parser classes
:rtype: list[type[rest_framework.parsers.BaseParser]]
"""
return get_object_classes(
call_view_method(self.view, "get_parsers", "parser_classes", [])
)
class PaginatorInspector(BaseInspector):
"""Base inspector for paginators.
Responsible for determining extra query parameters and response structure added by
given paginators.
"""
def get_paginator_parameters(self, paginator):
"""Get the pagination parameters for a single paginator **instance**.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `paginator`.
:param BasePagination paginator: the paginator
:rtype: list[openapi.Parameter]
"""
return NotHandled
def get_paginated_response(self, paginator, response_schema):
"""Add appropriate paging fields to a response :class:`.Schema`.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `paginator`.
:param BasePagination paginator: the paginator
:param openapi.Schema response_schema: the response schema that must be paged.
:rtype: openapi.Schema
"""
return NotHandled
class FilterInspector(BaseInspector):
"""Base inspector for filter backends.
Responsible for determining extra query parameters added by given filter backends.
"""
def get_filter_parameters(self, filter_backend):
"""Get the filter parameters for a single filter backend **instance**.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `filter_backend`.
:param BaseFilterBackend filter_backend: the filter backend
:rtype: list[openapi.Parameter]
"""
return NotHandled
class FieldInspector(BaseInspector):
"""Base inspector for serializers and serializer fields."""
def __init__(self, view, path, method, components, request, field_inspectors):
super(FieldInspector, self).__init__(view, path, method, components, request)
self.field_inspectors = field_inspectors
def add_manual_fields(self, serializer_or_field, schema):
"""Set fields from the ``swagger_schema_fields`` attribute on the Meta class.
This method is called only for serializers or fields that are converted into
``openapi.Schema`` objects.
:param serializer_or_field: serializer or field instance
:param openapi.Schema schema: the schema object to be modified in-place
"""
meta = getattr(serializer_or_field, "Meta", None)
swagger_schema_fields = getattr(meta, "swagger_schema_fields", {})
if swagger_schema_fields:
for attr, val in swagger_schema_fields.items():
setattr(schema, attr, val)
def field_to_swagger_object(
self, field, swagger_object_type, use_references, **kwargs
):
"""Convert a drf Serializer or Field instance into a Swagger object.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `field`.
:param rest_framework.serializers.Field field: the source field
:param type[openapi.SwaggerDict] swagger_object_type: should be one of Schema,
Parameter, Items
:param bool use_references: if False, forces all objects to be declared inline
instead of by referencing other components
:param kwargs: extra attributes for constructing the object;
if swagger_object_type is Parameter, ``name`` and ``in_`` should be provided
:return: the swagger object
:rtype: openapi.Parameter or openapi.Items or openapi.Schema or
openapi.SchemaRef
"""
return NotHandled
def probe_field_inspectors(
self, field, swagger_object_type, use_references, **kwargs
):
"""Helper method for recursively probing `field_inspectors` to handle a given
field.
All arguments are the same as :meth:`.field_to_swagger_object`.
:rtype: openapi.Parameter or openapi.Items or openapi.Schema or
openapi.SchemaRef
"""
return self.probe_inspectors(
self.field_inspectors,
"field_to_swagger_object",
field,
{"field_inspectors": self.field_inspectors},
swagger_object_type=swagger_object_type,
use_references=use_references,
**kwargs,
)
def _get_partial_types(self, field, swagger_object_type, use_references, **kwargs):
"""Helper method to extract generic information from a field and return a
partial constructor for the appropriate openapi object.
All arguments are the same as :meth:`.field_to_swagger_object`.
The return value is a tuple consisting of:
* a function for constructing objects of `swagger_object_type`; its prototype
is: ::
def SwaggerType(existing_object=None, **instance_kwargs):
This function creates an instance of `swagger_object_type`, passing the
following attributes to its init, in order of precedence:
- arguments specified by the ``kwargs`` parameter of
:meth:`._get_partial_types`
- ``instance_kwargs`` passed to the constructor function
- ``title``, ``description``, ``required``, ``x-nullable`` and ``default``
inferred from the field, where appropriate
If ``existing_object`` is not ``None``, it is updated instead of creating a
new object.
* a type that should be used for child objects if `field` is of an array type.
This can currently have two values:
- :class:`.Schema` if `swagger_object_type` is :class:`.Schema`
- :class:`.Items` if `swagger_object_type` is :class:`.Parameter` or
:class:`.Items`
:rtype: (function,type[openapi.Schema] or type[openapi.Items])
"""
assert swagger_object_type in (openapi.Schema, openapi.Parameter, openapi.Items)
assert not isinstance(field, openapi.SwaggerDict), (
"passed field is already a SwaggerDict object"
)
title = force_real_str(field.label) if field.label else None
title = (
title if swagger_object_type == openapi.Schema else None
) # only Schema has title
help_text = getattr(field, "help_text", None)
description = force_real_str(help_text) if help_text else None
description = (
description if swagger_object_type != openapi.Items else None
) # Items has no description either
def SwaggerType(existing_object=None, use_field_title=True, **instance_kwargs):
if (
"required" not in instance_kwargs
and swagger_object_type == openapi.Parameter
):
instance_kwargs["required"] = field.required
if (
"default" not in instance_kwargs
and swagger_object_type != openapi.Items
):
default = get_field_default(field)
if default not in (None, serializers.empty):
instance_kwargs["default"] = default
if (
use_field_title
and instance_kwargs.get("type", None) != openapi.TYPE_ARRAY
):
instance_kwargs.setdefault("title", title)
if description is not None:
instance_kwargs.setdefault("description", description)
if getattr(field, "allow_null", None):
instance_kwargs["x_nullable"] = True
instance_kwargs.update(kwargs)
if existing_object is not None:
assert isinstance(existing_object, swagger_object_type)
for key, val in sorted(instance_kwargs.items()):
setattr(existing_object, key, val)
result = existing_object
else:
result = swagger_object_type(**instance_kwargs)
# Provide an option to add manual parameters to a schema
# for example, to add examples
if swagger_object_type == openapi.Schema:
self.add_manual_fields(field, result)
return result
# arrays in Schema have Schema elements, arrays in Parameter and Items have
# Items elements
child_swagger_type = (
openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items
)
return SwaggerType, child_swagger_type
class SerializerInspector(FieldInspector):
def get_schema(self, serializer):
"""Convert a DRF Serializer instance to an :class:`.openapi.Schema`.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `serializer`.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:rtype: openapi.Schema
"""
return NotHandled
def get_request_parameters(self, serializer, in_):
"""Convert a DRF serializer into a list of :class:`.Parameter`\\ s.
Should return :data:`.NotHandled` if this inspector does not know how to handle
the given `serializer`.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:param str in_: the location of the parameters, one of the `openapi.IN_*`
constants
:rtype: list[openapi.Parameter]
"""
return NotHandled
class ViewInspector(BaseInspector):
body_methods = (
"PUT",
"PATCH",
"POST",
"DELETE",
) #: methods that are allowed to have a request body
#: methods that are assumed to require a request body determined by the view's
# ``serializer_class``
implicit_body_methods = ("PUT", "PATCH", "POST")
#: methods which are assumed to return a list of objects when present on non-detail
# endpoints
implicit_list_response_methods = ("GET",)
# real values set in __init__ to prevent import errors
field_inspectors = [] #:
filter_inspectors = [] #:
paginator_inspectors = [] #:
def __init__(self, view, path, method, components, request, overrides):
"""
Inspector class responsible for providing :class:`.Operation` definitions given
a view, path and method.
:param dict overrides: manual overrides as passed to
:func:`@swagger_auto_schema <.swagger_auto_schema>`
"""
super(ViewInspector, self).__init__(view, path, method, components, request)
self.overrides = overrides
self._prepend_inspector_overrides("field_inspectors")
self._prepend_inspector_overrides("filter_inspectors")
self._prepend_inspector_overrides("paginator_inspectors")
def _prepend_inspector_overrides(self, inspectors):
extra_inspectors = self.overrides.get(inspectors, None)
if extra_inspectors:
default_inspectors = [
insp
for insp in getattr(self, inspectors)
if insp not in extra_inspectors
]
setattr(self, inspectors, extra_inspectors + default_inspectors)
def get_operation(self, operation_keys):
"""Get an :class:`.Operation` for the given API endpoint (path, method).
This includes query, body parameters and response schemas.
:param tuple[str] operation_keys: an array of keys describing the hierarchical
layout of this view in the API; e.g. ``('snippets', 'list')``,
``('snippets', 'retrieve')``, etc.
:rtype: openapi.Operation
"""
raise NotImplementedError("ViewInspector must implement get_operation()!")
def is_list_view(self):
"""Determine whether this view is a list or a detail view. The difference
between the two is that detail views depend on a pk/id path parameter. Note that
a non-detail view does not necessarily imply a list response
(:meth:`.has_list_response`), nor are list responses limited to non-detail
views.
For example, one might have a `/topic/<pk>/posts` endpoint which is a detail
view that has a list response.
:rtype: bool"""
return is_list_view(self.path, self.method, self.view)
def has_list_response(self):
"""Determine whether this view returns multiple objects. By default this is any
non-detail view (see :meth:`.is_list_view`) whose request method is one of
:attr:`.implicit_list_response_methods`.
:rtype: bool
"""
return self.is_list_view() and (
self.method.upper() in self.implicit_list_response_methods
)
def should_filter(self):
"""Determine whether filter backend parameters should be included for this
request.
:rtype: bool
"""
return getattr(self.view, "filter_backends", None) and self.has_list_response()
def get_filter_parameters(self):
"""Return the parameters added to the view by its filter backends.
:rtype: list[openapi.Parameter]
"""
if not self.should_filter():
return []
fields = []
for filter_backend in getattr(self.view, "filter_backends"):
fields += (
self.probe_inspectors(
self.filter_inspectors, "get_filter_parameters", filter_backend()
)
or []
)
return fields
def should_page(self):
"""Determine whether paging parameters and structure should be added to this
operation's request and response.
:rtype: bool
"""
return getattr(self.view, "paginator", None) and self.has_list_response()
def get_pagination_parameters(self):
"""Return the parameters added to the view by its paginator.
:rtype: list[openapi.Parameter]
"""
if not self.should_page():
return []
return (
self.probe_inspectors(
self.paginator_inspectors,
"get_paginator_parameters",
getattr(self.view, "paginator"),
)
or []
)
def serializer_to_schema(self, serializer):
"""Convert a serializer to an OpenAPI :class:`.Schema`.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:returns: the converted :class:`.Schema`, or ``None`` in case of an unknown
serializer
:rtype: openapi.Schema or openapi.SchemaRef
"""
return self.probe_inspectors(
self.field_inspectors,
"get_schema",
serializer,
{"field_inspectors": self.field_inspectors},
)
def serializer_to_parameters(self, serializer, in_):
"""Convert a serializer to a possibly empty list of :class:`.Parameter`\\ s.
:param serializers.BaseSerializer serializer: the ``Serializer`` instance
:param str in_: the location of the parameters, one of the `openapi.IN_*`
constants
:rtype: list[openapi.Parameter]
"""
return (
self.probe_inspectors(
self.field_inspectors,
"get_request_parameters",
serializer,
{"field_inspectors": self.field_inspectors},
in_=in_,
)
or []
)
def get_paginated_response(self, response_schema):
"""Add appropriate paging fields to a response :class:`.Schema`.
:param openapi.Schema response_schema: the response schema that must be paged.
:returns: the paginated response class:`.Schema`, or ``None`` in case of an
unknown pagination scheme
:rtype: openapi.Schema
"""
return self.probe_inspectors(
self.paginator_inspectors,
"get_paginated_response",
getattr(self.view, "paginator"),
response_schema=response_schema,
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
from collections import OrderedDict
from functools import wraps
try:
import coreschema
except ImportError:
coreschema = None
from .. import openapi
from ..utils import force_real_str
from .base import FilterInspector, NotHandled, PaginatorInspector
def ignore_assert_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except AssertionError:
return NotHandled
return wrapper
class DrfAPICompatInspector(PaginatorInspector, FilterInspector):
def param_to_schema(self, param):
return openapi.Parameter(
name=param["name"],
in_=param["in"],
description=param.get("description"),
required=param.get("required", False),
**param["schema"],
)
def get_paginator_parameters(self, paginator):
if hasattr(paginator, "get_schema_operation_parameters"):
return list(
map(
self.param_to_schema,
paginator.get_schema_operation_parameters(self.view),
)
)
return NotHandled
def get_filter_parameters(self, filter_backend):
if hasattr(filter_backend, "get_schema_operation_parameters"):
return list(
map(
self.param_to_schema,
filter_backend.get_schema_operation_parameters(self.view),
)
)
if hasattr(filter_backend, "get_filterset_class"):
return self.get_filter_backend_params(filter_backend)
return NotHandled
def get_filter_backend_params(self, filter_backend):
filterset_class = filter_backend.get_filterset_class(
self.view,
self.view.queryset,
)
if filterset_class is None:
return NotHandled
return [
openapi.Parameter(
name=field_name,
in_=openapi.IN_QUERY,
required=field.extra.get("required", False),
type=openapi.TYPE_STRING,
description=str(field.label) if field.label else "",
)
for field_name, field in filterset_class.base_filters.items()
]
class CoreAPICompatInspector(PaginatorInspector, FilterInspector):
"""Converts ``coreapi.Field``\\ s to :class:`.openapi.Parameter`\\ s for filters and
paginators that implement a ``get_schema_fields`` method.
"""
@ignore_assert_decorator
def get_paginator_parameters(self, paginator):
fields = []
if hasattr(paginator, "get_schema_fields"):
fields = paginator.get_schema_fields(self.view)
return [self.coreapi_field_to_parameter(field) for field in fields]
@ignore_assert_decorator
def get_filter_parameters(self, filter_backend):
fields = []
if hasattr(filter_backend, "get_schema_fields"):
fields = filter_backend.get_schema_fields(self.view)
return [self.coreapi_field_to_parameter(field) for field in fields]
def coreapi_field_to_parameter(self, field):
"""Convert an instance of `coreapi.Field` to a swagger :class:`.Parameter`
object.
:param coreapi.Field field:
:rtype: openapi.Parameter
"""
location_to_in = {
"query": openapi.IN_QUERY,
"path": openapi.IN_PATH,
"form": openapi.IN_FORM,
"body": openapi.IN_FORM,
}
coreapi_types = {
coreschema.Integer: openapi.TYPE_INTEGER,
coreschema.Number: openapi.TYPE_NUMBER,
coreschema.String: openapi.TYPE_STRING,
coreschema.Boolean: openapi.TYPE_BOOLEAN,
}
coreschema_attrs = ["format", "pattern", "enum", "min_length", "max_length"]
schema = field.schema
return openapi.Parameter(
name=field.name,
in_=location_to_in[field.location],
required=field.required,
description=force_real_str(schema.description) if schema else None,
type=coreapi_types.get(type(schema), openapi.TYPE_STRING),
**OrderedDict(
(attr, getattr(schema, attr, None)) for attr in coreschema_attrs
),
)
class DjangoRestResponsePagination(PaginatorInspector):
"""Provides response schema pagination wrapping for django-rest-framework'
LimitOffsetPagination, PageNumberPagination and CursorPagination
"""
def fix_paginated_property(self, key: str, value: dict):
# Need to remove useless params from schema
value.pop("example", None)
if "nullable" in value:
value["x-nullable"] = value.pop("nullable")
if key in {"next", "previous"} and "format" not in value:
value["format"] = "uri"
return openapi.Schema(**value)
def get_paginated_response(self, paginator, response_schema):
if hasattr(paginator, "get_paginated_response_schema"):
paginator_schema = paginator.get_paginated_response_schema(response_schema)
if paginator_schema["type"] == openapi.TYPE_OBJECT:
properties = {
k: self.fix_paginated_property(k, v)
for k, v in paginator_schema.pop("properties").items()
}
if "required" not in paginator_schema:
paginator_schema.setdefault("required", [])
for prop in ("count", "results"):
if prop in properties:
paginator_schema["required"].append(prop)
return openapi.Schema(**paginator_schema, properties=properties)
else:
return openapi.Schema(**paginator_schema)
return response_schema

View File

@@ -0,0 +1,485 @@
import logging
from collections import OrderedDict
from rest_framework.request import is_form_media_type
from rest_framework.schemas import AutoSchema
from rest_framework.status import is_success
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import (
filter_none,
force_real_str,
force_serializer_instance,
get_consumes,
get_produces,
guess_response_status,
merge_params,
no_body,
param_list_to_odict,
)
from .base import ViewInspector, call_view_method
logger = logging.getLogger(__name__)
class SwaggerAutoSchema(ViewInspector):
def __init__(
self, view, path, method, components, request, overrides, operation_keys=None
):
super(SwaggerAutoSchema, self).__init__(
view, path, method, components, request, overrides
)
self._sch = AutoSchema()
self._sch.view = view
self.operation_keys = operation_keys
def get_operation(self, operation_keys=None):
operation_keys = operation_keys or self.operation_keys
consumes = self.get_consumes()
produces = self.get_produces()
body = self.get_request_body_parameters(consumes)
query = self.get_query_parameters()
parameters = body + query
parameters = filter_none(parameters)
parameters = self.add_manual_parameters(parameters)
operation_id = self.get_operation_id(operation_keys)
summary, description = self.get_summary_and_description()
security = self.get_security()
assert security is None or isinstance(security, list), (
"security must be a list of security requirement objects"
)
deprecated = self.is_deprecated()
tags = self.get_tags(operation_keys)
responses = self.get_responses()
return openapi.Operation(
operation_id=operation_id,
description=force_real_str(description),
summary=force_real_str(summary),
responses=responses,
parameters=parameters,
consumes=consumes,
produces=produces,
tags=tags,
security=security,
deprecated=deprecated,
)
def get_request_body_parameters(self, consumes):
"""Return the request body parameters for this view. |br|
This is either:
- a list with a single object Parameter with a :class:`.Schema` derived from
the request serializer
- a list of primitive Parameters parsed as form data
:param list[str] consumes: a list of accepted MIME types as returned by
:meth:`.get_consumes`
:return: a (potentially empty) list of :class:`.Parameter`\\ s either
``in: body`` or ``in: formData``
:rtype: list[openapi.Parameter]
"""
serializer = self.get_request_serializer()
schema = None
if serializer is None:
return []
if isinstance(serializer, openapi.Schema.OR_REF):
schema = serializer
if any(is_form_media_type(encoding) for encoding in consumes):
if schema is not None:
raise SwaggerGenerationError("form request body cannot be a Schema")
return self.get_request_form_parameters(serializer)
else:
if schema is None:
schema = self.get_request_body_schema(serializer)
return [self.make_body_parameter(schema)] if schema is not None else []
def get_view_serializer(self):
"""Return the serializer as defined by the view's ``get_serializer()`` method.
:return: the view's ``Serializer``
:rtype: rest_framework.serializers.Serializer
"""
return call_view_method(self.view, "get_serializer")
def _get_request_body_override(self):
"""Parse the request_body key in the override dict. This method is not public
API."""
body_override = self.overrides.get("request_body", None)
if body_override is not None:
if body_override is no_body:
return no_body
if self.method not in self.body_methods:
raise SwaggerGenerationError(
"request_body can only be applied to ("
+ ",".join(self.body_methods)
+ "); are you looking for query_serializer or manual_parameters?"
)
if isinstance(body_override, openapi.Schema.OR_REF):
return body_override
return force_serializer_instance(body_override)
return body_override
def get_request_serializer(self):
"""Return the request serializer (used for parsing the request payload) for this
endpoint.
:return: the request serializer, or one of :class:`.Schema`,
:class:`.SchemaRef`, ``None``
:rtype: rest_framework.serializers.Serializer
"""
body_override = self._get_request_body_override()
if body_override is None and self.method in self.implicit_body_methods:
return self.get_view_serializer()
if body_override is no_body:
return None
return body_override
def get_request_form_parameters(self, serializer):
"""Given a Serializer, return a list of ``in: formData``
:class:`.Parameter`\\ s.
:param serializer: the view's request serializer as returned by
:meth:`.get_request_serializer`
:rtype: list[openapi.Parameter]
"""
return self.serializer_to_parameters(serializer, in_=openapi.IN_FORM)
def get_request_body_schema(self, serializer):
"""Return the :class:`.Schema` for a given request's body data. Only applies to
PUT, PATCH and POST requests.
:param serializer: the view's request serializer as returned by
:meth:`.get_request_serializer`
:rtype: openapi.Schema
"""
return self.serializer_to_schema(serializer)
def make_body_parameter(self, schema):
"""Given a :class:`.Schema` object, create an ``in: body`` :class:`.Parameter`.
:param openapi.Schema schema: the request body schema
:rtype: openapi.Parameter
"""
return openapi.Parameter(
name="data", in_=openapi.IN_BODY, required=True, schema=schema
)
def add_manual_parameters(self, parameters):
"""Add/replace parameters from the given list of automatically generated request
parameters.
:param list[openapi.Parameter] parameters: generated parameters
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
manual_parameters = self.overrides.get("manual_parameters", None) or []
if any(
param.in_ == openapi.IN_BODY for param in manual_parameters
): # pragma: no cover
raise SwaggerGenerationError(
"specify the body parameter as a Schema or Serializer in request_body"
)
if any(
param.in_ == openapi.IN_FORM for param in manual_parameters
): # pragma: no cover
has_body_parameter = any(
param.in_ == openapi.IN_BODY for param in parameters
)
if has_body_parameter or not any(
is_form_media_type(encoding) for encoding in self.get_consumes()
):
raise SwaggerGenerationError(
"cannot add form parameters when the request has a request body; "
"did you forget to set an appropriate parser class on the view?"
)
if self.method not in self.body_methods:
raise SwaggerGenerationError(
"form parameters can only be applied to "
"(" + ",".join(self.body_methods) + ") HTTP methods"
)
return merge_params(parameters, manual_parameters)
def get_responses(self):
"""Get the possible responses for this view as a swagger :class:`.Responses`
object.
:return: the documented responses
:rtype: openapi.Responses
"""
response_serializers = self.get_response_serializers()
return openapi.Responses(
responses=self.get_response_schemas(response_serializers)
)
def get_default_response_serializer(self):
"""Return the default response serializer for this endpoint. This is derived
from either the ``request_body`` override or the request serializer
(:meth:`.get_view_serializer`).
:return: response serializer, :class:`.Schema`, :class:`.SchemaRef`, ``None``
"""
body_override = self._get_request_body_override()
if body_override and body_override is not no_body:
return body_override
return self.get_view_serializer()
def get_default_responses(self):
"""Get the default responses determined for this view from the request
serializer and request method.
:type: dict[str, openapi.Schema]
"""
method = self.method.lower()
default_status = guess_response_status(method)
default_schema = ""
if method in ("get", "post", "put", "patch"):
default_schema = self.get_default_response_serializer()
default_schema = default_schema or ""
if default_schema and not isinstance(default_schema, openapi.Schema):
default_schema = self.serializer_to_schema(default_schema) or ""
if default_schema:
if self.has_list_response():
default_schema = openapi.Schema(
type=openapi.TYPE_ARRAY, items=default_schema
)
if self.should_page():
default_schema = (
self.get_paginated_response(default_schema) or default_schema
)
return OrderedDict({str(default_status): default_schema})
def get_response_serializers(self):
"""Return the response codes that this view is expected to return, and the
serializer for each response body. The return value should be a dict where the
keys are possible status codes, and values are either strings,
``Serializer``\\ s, :class:`.Schema`, :class:`.SchemaRef` or :class:`.Response`
objects. See
:func:`@swagger_auto_schema <.swagger_auto_schema>` for more details.
:return: the response serializers
:rtype: dict
"""
manual_responses = self.overrides.get("responses", None) or {}
manual_responses = OrderedDict(
(str(sc), resp) for sc, resp in manual_responses.items()
)
responses = OrderedDict()
if not any(is_success(int(sc)) for sc in manual_responses if sc != "default"):
responses = self.get_default_responses()
responses.update((str(sc), resp) for sc, resp in manual_responses.items())
return responses
def get_response_schemas(self, response_serializers):
"""Return the :class:`.openapi.Response` objects calculated for this view.
:param dict response_serializers: response serializers as returned by
:meth:`.get_response_serializers`
:return: a dictionary of status code to :class:`.Response` object
:rtype: dict[str, openapi.Response]
"""
responses = OrderedDict()
for sc, serializer in response_serializers.items():
if isinstance(serializer, str):
response = openapi.Response(description=force_real_str(serializer))
elif not serializer:
continue
elif isinstance(serializer, openapi.Response):
response = serializer
if hasattr(response, "schema") and not isinstance(
response.schema, openapi.Schema.OR_REF
):
serializer = force_serializer_instance(response.schema)
response.schema = self.serializer_to_schema(serializer)
elif isinstance(serializer, openapi.Schema.OR_REF):
response = openapi.Response(
description="",
schema=serializer,
)
elif isinstance(serializer, openapi._Ref):
response = serializer
else:
serializer = force_serializer_instance(serializer)
response = openapi.Response(
description="",
schema=self.serializer_to_schema(serializer),
)
responses[str(sc)] = response
return responses
def get_query_serializer(self):
"""Return the query serializer (used for parsing query parameters) for this
endpoint.
:return: the query serializer, or ``None``
"""
query_serializer = self.overrides.get("query_serializer", None)
if query_serializer is not None:
query_serializer = force_serializer_instance(query_serializer)
return query_serializer
def get_query_parameters(self):
"""Return the query parameters accepted by this view.
:rtype: list[openapi.Parameter]
"""
natural_parameters = (
self.get_filter_parameters() + self.get_pagination_parameters()
)
query_serializer = self.get_query_serializer()
serializer_parameters = []
if query_serializer is not None:
serializer_parameters = self.serializer_to_parameters(
query_serializer, in_=openapi.IN_QUERY
)
if (
len(
set(param_list_to_odict(natural_parameters))
& set(param_list_to_odict(serializer_parameters))
)
!= 0
):
raise SwaggerGenerationError(
"your query_serializer contains fields that conflict with the "
"filter_backend or paginator_class on the view - %s %s"
% (self.method, self.path)
)
return natural_parameters + serializer_parameters
def get_operation_id(self, operation_keys=None):
"""Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API.
:param tuple[str] operation_keys: an array of keys derived from the path
describing the hierarchical layout of this view in the API; e.g.
``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str
"""
operation_keys = operation_keys or self.operation_keys
operation_id = self.overrides.get("operation_id", "")
if not operation_id:
operation_id = "_".join(operation_keys)
return operation_id
def split_summary_from_description(self, description):
"""Decide if and how to split a summary out of the given description. The
default implementation uses the first paragraph of the description as a summary
if it is less than 120 characters long.
:param description: the full description to be analyzed
:return: summary and description
:rtype: (str,str)
"""
# https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings
summary = None
summary_max_len = (
120 # OpenAPI 2.0 spec says summary should be under 120 characters
)
sections = description.split("\n\n", 1)
if len(sections) == 2:
sections[0] = sections[0].strip()
if len(sections[0]) < summary_max_len:
summary, description = sections
description = description.strip()
return summary, description
def get_summary_and_description(self):
"""Return an operation summary and description determined from the view's
docstring.
:return: summary and description
:rtype: (str,str)
"""
description = self.overrides.get("operation_description", None)
summary = self.overrides.get("operation_summary", None)
if description is None:
description = self._sch.get_description(self.path, self.method) or ""
description = description.strip().replace("\r", "")
if description and (summary is None):
# description from docstring... do summary magic
summary, description = self.split_summary_from_description(description)
return summary, description
def get_security(self):
"""Return a list of security requirements for this operation.
Returning an empty list marks the endpoint as unauthenticated (i.e. removes all
accepted authentication schemes). Returning ``None`` will inherit the top-level
security requirements.
:return: security requirements
:rtype: list[dict[str,list[str]]]"""
return self.overrides.get("security", None)
def is_deprecated(self):
"""Return ``True`` if this operation is to be marked as deprecated.
:return: deprecation status
:rtype: bool
"""
return self.overrides.get("deprecated", None)
def get_tags(self, operation_keys=None):
"""Get a list of tags for this operation. Tags determine how operations relate
with each other, and in the UI each tag will show as a group containing the
operations that use it. If not provided in overrides, tags will be inferred
from the operation url.
:param tuple[str] operation_keys: an array of keys derived from the path
describing the hierarchical layout of this view in the API; e.g.
``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str]
"""
operation_keys = operation_keys or self.operation_keys
tags = self.overrides.get("tags")
if not tags:
tags = [operation_keys[0]]
return tags
def get_consumes(self):
"""Return the MIME types this endpoint can consume.
:rtype: list[str]
"""
return self.overrides.get("consumes") or get_consumes(self.get_parser_classes())
def get_produces(self):
"""Return the MIME types this endpoint can produce.
:rtype: list[str]
"""
return self.overrides.get("produces") or get_produces(
self.get_renderer_classes()
)