openapi.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. import collections
  2. import logging
  3. import re
  4. import urllib.parse as urlparse
  5. from collections import OrderedDict
  6. from django.urls import get_script_prefix
  7. from django.utils.functional import Promise
  8. from inflection import camelize
  9. from .utils import dict_has_ordered_keys, filter_none, force_real_str
  10. try:
  11. from collections import abc as collections_abc
  12. except ImportError:
  13. collections_abc = collections
  14. logger = logging.getLogger(__name__)
  15. TYPE_OBJECT = "object" #:
  16. TYPE_STRING = "string" #:
  17. TYPE_NUMBER = "number" #:
  18. TYPE_INTEGER = "integer" #:
  19. TYPE_BOOLEAN = "boolean" #:
  20. TYPE_ARRAY = "array" #:
  21. TYPE_FILE = "file" #:
  22. # officially supported by Swagger 2.0 spec
  23. FORMAT_DATE = "date" #:
  24. FORMAT_DATETIME = "date-time" #:
  25. FORMAT_PASSWORD = "password" #:
  26. FORMAT_BINARY = "binary" #:
  27. FORMAT_BASE64 = "bytes" #:
  28. FORMAT_FLOAT = "float" #:
  29. FORMAT_DOUBLE = "double" #:
  30. FORMAT_INT32 = "int32" #:
  31. FORMAT_INT64 = "int64" #:
  32. # defined in JSON-schema
  33. FORMAT_EMAIL = "email" #:
  34. FORMAT_IPV4 = "ipv4" #:
  35. FORMAT_IPV6 = "ipv6" #:
  36. FORMAT_URI = "uri" #:
  37. # pulled out of my ass
  38. FORMAT_UUID = "uuid" #:
  39. FORMAT_SLUG = "slug" #:
  40. FORMAT_DECIMAL = "decimal"
  41. IN_BODY = 'body' #:
  42. IN_PATH = 'path' #:
  43. IN_QUERY = 'query' #:
  44. IN_FORM = 'formData' #:
  45. IN_HEADER = 'header' #:
  46. SCHEMA_DEFINITIONS = 'definitions' #:
  47. def make_swagger_name(attribute_name):
  48. """
  49. Convert a python variable name into a Swagger spec attribute name.
  50. In particular,
  51. * if name starts with ``x_``, return ``x-{camelCase}``
  52. * if name is ``ref``, return ``$ref``
  53. * else return the name converted to camelCase, with trailing underscores stripped
  54. :param str attribute_name: python attribute name
  55. :return: swagger name
  56. """
  57. if attribute_name == 'ref':
  58. return "$ref"
  59. if attribute_name.startswith("x_"):
  60. return "x-" + camelize(attribute_name[2:], uppercase_first_letter=False)
  61. return camelize(attribute_name.rstrip('_'), uppercase_first_letter=False)
  62. def _bare_SwaggerDict(cls):
  63. assert issubclass(cls, SwaggerDict)
  64. result = cls.__new__(cls)
  65. OrderedDict.__init__(result) # no __init__ called for SwaggerDict subclasses!
  66. return result
  67. class SwaggerDict(OrderedDict):
  68. """A particular type of OrderedDict, which maps all attribute accesses to dict lookups using
  69. :func:`.make_swagger_name`. Attribute names starting with ``_`` are set on the object as-is and are not included
  70. in the specification output.
  71. Used as a base class for all Swagger helper models.
  72. """
  73. def __init__(self, **attrs):
  74. super(SwaggerDict, self).__init__()
  75. self._extras__ = attrs
  76. if type(self) == SwaggerDict:
  77. self._insert_extras__()
  78. def __setattr__(self, key, value):
  79. if key.startswith('_'):
  80. super(SwaggerDict, self).__setattr__(key, value)
  81. return
  82. if value is not None:
  83. self[make_swagger_name(key)] = value
  84. def __getattr__(self, item):
  85. if item.startswith('_'):
  86. raise AttributeError
  87. try:
  88. return self[make_swagger_name(item)]
  89. except KeyError:
  90. # raise_from is EXTREMELY slow, replaced with plain raise
  91. raise AttributeError("object of class " + type(self).__name__ + " has no attribute " + item)
  92. def __delattr__(self, item):
  93. if item.startswith('_'):
  94. super(SwaggerDict, self).__delattr__(item)
  95. return
  96. del self[make_swagger_name(item)]
  97. def _insert_extras__(self):
  98. """
  99. From an ordering perspective, it is desired that extra attributes such as vendor extensions stay at the
  100. bottom of the object. However, python2.7's OrderedDict craps out if you try to insert into it before calling
  101. init. This means that subclasses must call super().__init__ as the first statement of their own __init__,
  102. which would result in the extra attributes being added first. For this reason, we defer the insertion of the
  103. attributes and require that subclasses call ._insert_extras__ at the end of their __init__ method.
  104. """
  105. for attr, val in sorted(self._extras__.items()):
  106. setattr(self, attr, val)
  107. @staticmethod
  108. def _as_odict(obj, memo):
  109. """Implementation detail of :meth:`.as_odict`"""
  110. if id(obj) in memo:
  111. return memo[id(obj)]
  112. if isinstance(obj, Promise) and hasattr(obj, '_proxy____cast'):
  113. # handle __proxy__ objects from django.utils.functional.lazy
  114. obj = obj._proxy____cast()
  115. if isinstance(obj, collections_abc.Mapping):
  116. result = OrderedDict()
  117. memo[id(obj)] = result
  118. items = obj.items()
  119. if not dict_has_ordered_keys(obj):
  120. items = sorted(items)
  121. for attr, val in items:
  122. result[attr] = SwaggerDict._as_odict(val, memo)
  123. return result
  124. elif isinstance(obj, str):
  125. return force_real_str(obj)
  126. elif isinstance(obj, collections_abc.Iterable) and not isinstance(obj, collections_abc.Iterator):
  127. return type(obj)(SwaggerDict._as_odict(elem, memo) for elem in obj)
  128. return obj
  129. def as_odict(self):
  130. """Convert this object into an ``OrderedDict`` instance.
  131. :rtype: OrderedDict
  132. """
  133. return SwaggerDict._as_odict(self, {})
  134. def __reduce__(self):
  135. # for pickle support; this skips calls to all SwaggerDict __init__ methods and relies
  136. # on the already set attributes instead
  137. attrs = {k: v for k, v in vars(self).items() if not k.startswith('_NP_')}
  138. return _bare_SwaggerDict, (type(self),), attrs, None, iter(self.items())
  139. class Contact(SwaggerDict):
  140. def __init__(self, name=None, url=None, email=None, **extra):
  141. """Swagger Contact object
  142. At least one of the following fields is required:
  143. :param str name: contact name
  144. :param str url: contact url
  145. :param str email: contact e-mail
  146. """
  147. super(Contact, self).__init__(**extra)
  148. if name is None and url is None and email is None:
  149. raise AssertionError("one of name, url or email is requires for Swagger Contact object")
  150. self.name = name
  151. self.url = url
  152. self.email = email
  153. self._insert_extras__()
  154. class License(SwaggerDict):
  155. def __init__(self, name, url=None, **extra):
  156. """Swagger License object
  157. :param str name: Required. License name
  158. :param str url: link to detailed license information
  159. """
  160. super(License, self).__init__(**extra)
  161. if name is None:
  162. raise AssertionError("name is required for Swagger License object")
  163. self.name = name
  164. self.url = url
  165. self._insert_extras__()
  166. class Info(SwaggerDict):
  167. def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None,
  168. **extra):
  169. """Swagger Info object
  170. :param str title: Required. API title.
  171. :param str default_version: Required. API version string (not to be confused with Swagger spec version)
  172. :param str description: API description; markdown supported
  173. :param str terms_of_service: API terms of service; should be a URL
  174. :param Contact contact: contact object
  175. :param License license: license object
  176. """
  177. super(Info, self).__init__(**extra)
  178. if title is None or default_version is None:
  179. raise AssertionError("title and version are required for Swagger info object")
  180. if contact is not None and not isinstance(contact, Contact):
  181. raise AssertionError("contact must be a Contact object")
  182. if license is not None and not isinstance(license, License):
  183. raise AssertionError("license must be a License object")
  184. self.title = title
  185. self._default_version = default_version
  186. self.description = description
  187. self.terms_of_service = terms_of_service
  188. self.contact = contact
  189. self.license = license
  190. self._insert_extras__()
  191. class Swagger(SwaggerDict):
  192. def __init__(self, info=None, _url=None, _prefix=None, _version=None, consumes=None, produces=None,
  193. security_definitions=None, security=None, paths=None, definitions=None, **extra):
  194. """Root Swagger object.
  195. :param .Info info: info object
  196. :param str _url: URL used for setting the API host and scheme
  197. :param str _prefix: api path prefix to use in setting basePath; this will be appended to the wsgi
  198. SCRIPT_NAME prefix or Django's FORCE_SCRIPT_NAME if applicable
  199. :param str _version: version string to override Info
  200. :param dict[str,dict] security_definitions: list of supported authentication mechanisms
  201. :param list[dict[str,list[str]]] security: authentication mechanisms accepted globally
  202. :param list[str] consumes: consumed MIME types; can be overridden in Operation
  203. :param list[str] produces: produced MIME types; can be overridden in Operation
  204. :param Paths paths: paths object
  205. :param dict[str,Schema] definitions: named models
  206. """
  207. super(Swagger, self).__init__(**extra)
  208. self.swagger = '2.0'
  209. self.info = info
  210. self.info.version = _version or info._default_version
  211. if _url:
  212. url = urlparse.urlparse(_url)
  213. assert url.netloc and url.scheme, "if given, url must have both schema and netloc"
  214. self.host = url.netloc
  215. self.schemes = [url.scheme]
  216. self.base_path = self.get_base_path(get_script_prefix(), _prefix)
  217. self.consumes = consumes
  218. self.produces = produces
  219. self.security_definitions = filter_none(security_definitions)
  220. self.security = filter_none(security)
  221. self.paths = paths
  222. self.definitions = filter_none(definitions)
  223. self._insert_extras__()
  224. @classmethod
  225. def get_base_path(cls, script_prefix, api_prefix):
  226. """Determine an appropriate value for ``basePath`` based on the SCRIPT_NAME and the api common prefix.
  227. :param str script_prefix: script prefix as defined by django ``get_script_prefix``
  228. :param str api_prefix: api common prefix
  229. :return: joined base path
  230. """
  231. # avoid double slash when joining script_name with api_prefix
  232. if script_prefix and script_prefix.endswith('/'):
  233. script_prefix = script_prefix[:-1]
  234. if not api_prefix.startswith('/'):
  235. api_prefix = '/' + api_prefix
  236. base_path = script_prefix + api_prefix
  237. # ensure that the base path has a leading slash and no trailing slash
  238. if base_path and base_path.endswith('/'):
  239. base_path = base_path[:-1]
  240. if not base_path.startswith('/'):
  241. base_path = '/' + base_path
  242. return base_path
  243. class Paths(SwaggerDict):
  244. def __init__(self, paths, **extra):
  245. """A listing of all the paths in the API.
  246. :param dict[str,PathItem] paths:
  247. """
  248. super(Paths, self).__init__(**extra)
  249. for path, path_obj in paths.items():
  250. assert path.startswith("/")
  251. if path_obj is not None: # pragma: no cover
  252. self[path] = path_obj
  253. self._insert_extras__()
  254. class PathItem(SwaggerDict):
  255. OPERATION_NAMES = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']
  256. def __init__(self, get=None, put=None, post=None, delete=None, options=None,
  257. head=None, patch=None, parameters=None, **extra):
  258. """Information about a single path
  259. :param Operation get: operation for GET
  260. :param Operation put: operation for PUT
  261. :param Operation post: operation for POST
  262. :param Operation delete: operation for DELETE
  263. :param Operation options: operation for OPTIONS
  264. :param Operation head: operation for HEAD
  265. :param Operation patch: operation for PATCH
  266. :param list[Parameter] parameters: parameters that apply to all operations
  267. """
  268. super(PathItem, self).__init__(**extra)
  269. self.get = get
  270. self.head = head
  271. self.post = post
  272. self.put = put
  273. self.patch = patch
  274. self.delete = delete
  275. self.options = options
  276. self.parameters = filter_none(parameters)
  277. self._insert_extras__()
  278. @property
  279. def operations(self):
  280. """A list of all standard Operations on this PathItem object. See :attr:`.OPERATION_NAMES`.
  281. :return: list of (method name, Operation) tuples
  282. :rtype: list[tuple[str,Operation]]
  283. """
  284. return [(k, v) for k, v in self.items() if k in PathItem.OPERATION_NAMES and v]
  285. class Operation(SwaggerDict):
  286. def __init__(self, operation_id, responses, parameters=None, consumes=None, produces=None, summary=None,
  287. description=None, tags=None, security=None, **extra):
  288. """Information about an API operation (path + http method combination)
  289. :param str operation_id: operation ID, should be unique across all operations
  290. :param Responses responses: responses returned
  291. :param list[Parameter] parameters: parameters accepted
  292. :param list[str] consumes: content types accepted
  293. :param list[str] produces: content types produced
  294. :param str summary: operation summary; should be < 120 characters
  295. :param str description: operation description; can be of any length and supports markdown
  296. :param list[str] tags: operation tags
  297. :param list[dict[str,list[str]]] security: list of security requirements
  298. """
  299. super(Operation, self).__init__(**extra)
  300. self.operation_id = operation_id
  301. self.summary = summary
  302. self.description = description
  303. self.parameters = filter_none(parameters)
  304. self.responses = responses
  305. self.consumes = filter_none(consumes)
  306. self.produces = filter_none(produces)
  307. self.tags = filter_none(tags)
  308. self.security = filter_none(security)
  309. self._insert_extras__()
  310. def _check_type(type, format, enum, pattern, items, _obj_type):
  311. if items and type != TYPE_ARRAY:
  312. raise AssertionError("items can only be used when type is array")
  313. if type == TYPE_ARRAY and not items:
  314. raise AssertionError("TYPE_ARRAY requires the items attribute")
  315. if pattern and type != TYPE_STRING:
  316. raise AssertionError("pattern can only be used when type is string")
  317. if (format or enum or pattern) and type in (TYPE_OBJECT, TYPE_ARRAY, None):
  318. raise AssertionError("[format, enum, pattern] can only be applied to primitive " + _obj_type)
  319. class Items(SwaggerDict):
  320. def __init__(self, type=None, format=None, enum=None, pattern=None, items=None, **extra):
  321. """Used when defining an array :class:`.Parameter` to describe the array elements.
  322. :param str type: type of the array elements; must not be ``object``
  323. :param str format: value format, see OpenAPI spec
  324. :param list enum: restrict possible values
  325. :param str pattern: pattern if type is ``string``
  326. :param .Items items: only valid if `type` is ``array``
  327. """
  328. super(Items, self).__init__(**extra)
  329. assert type is not None, "type is required!"
  330. self.type = type
  331. self.format = format
  332. self.enum = enum
  333. self.pattern = pattern
  334. self.items_ = items
  335. self._insert_extras__()
  336. _check_type(type, format, enum, pattern, items, self.__class__)
  337. class Parameter(SwaggerDict):
  338. def __init__(self, name, in_, description=None, required=None, schema=None,
  339. type=None, format=None, enum=None, pattern=None, items=None, default=None, **extra):
  340. """Describe parameters accepted by an :class:`.Operation`. Each parameter should be a unique combination of
  341. (`name`, `in_`). ``body`` and ``form`` parameters in the same operation are mutually exclusive.
  342. :param str name: parameter name
  343. :param str in_: parameter location
  344. :param str description: parameter description
  345. :param bool required: whether the parameter is required for the operation
  346. :param schema: required if `in_` is ``body``
  347. :type schema: Schema or SchemaRef
  348. :param str type: parameter type; required if `in_` is not ``body``; must not be ``object``
  349. :param str format: value format, see OpenAPI spec
  350. :param list enum: restrict possible values
  351. :param str pattern: pattern if type is ``string``
  352. :param .Items items: only valid if `type` is ``array``
  353. :param default: default value if the parameter is not provided; must conform to parameter type
  354. """
  355. super(Parameter, self).__init__(**extra)
  356. self.name = name
  357. self.in_ = in_
  358. self.description = description
  359. self.required = required
  360. self.schema = schema
  361. self.type = type
  362. self.format = format
  363. self.enum = enum
  364. self.pattern = pattern
  365. self.items_ = items
  366. self.default = default
  367. self._insert_extras__()
  368. if (not schema and not type) or (schema and type):
  369. raise AssertionError("either schema or type are required for Parameter object (not both)!")
  370. if schema and isinstance(schema, Schema):
  371. schema._remove_read_only()
  372. if self['in'] == IN_PATH:
  373. # path parameters must always be required
  374. assert required is not False, "path parameter cannot be optional"
  375. self.required = True
  376. if self['in'] != IN_BODY and schema is not None:
  377. raise AssertionError("schema can only be applied to a body Parameter, not %s" % type)
  378. if default and not type:
  379. raise AssertionError("default can only be applied to a non-body Parameter")
  380. _check_type(type, format, enum, pattern, items, self.__class__)
  381. class Schema(SwaggerDict):
  382. OR_REF = () #: useful for type-checking, e.g ``isinstance(obj, openapi.Schema.OR_REF)``
  383. def __init__(self, title=None, description=None, type=None, format=None, enum=None, pattern=None, properties=None,
  384. additional_properties=None, required=None, items=None, default=None, read_only=None, **extra):
  385. """Describes a complex object accepted as parameter or returned as a response.
  386. :param str title: schema title
  387. :param str description: schema description
  388. :param str type: value type; required
  389. :param str format: value format, see OpenAPI spec
  390. :param list enum: restrict possible values
  391. :param str pattern: pattern if type is ``string``
  392. :param properties: object properties; required if `type` is ``object``
  393. :type properties: dict[str,Schema or SchemaRef]
  394. :param additional_properties: allow wildcard properties not listed in `properties`
  395. :type additional_properties: bool or Schema or SchemaRef
  396. :param list[str] required: list of required property names
  397. :param items: type of array items, only valid if `type` is ``array``
  398. :type items: Schema or SchemaRef
  399. :param default: only valid when insider another ``Schema``\\ 's ``properties``;
  400. the default value of this property if it is not provided, must conform to the type of this Schema
  401. :param read_only: only valid when insider another ``Schema``\\ 's ``properties``;
  402. declares the property as read only - it must only be sent as part of responses, never in requests
  403. """
  404. super(Schema, self).__init__(**extra)
  405. if required is True or required is False:
  406. # common error
  407. raise AssertionError("the `required` attribute of schema must be an "
  408. "array of required property names, not a boolean!")
  409. assert type, "type is required!"
  410. self.title = title
  411. self.description = description
  412. self.required = filter_none(required)
  413. self.type = type
  414. self.properties = filter_none(properties)
  415. self.additional_properties = additional_properties
  416. self.format = format
  417. self.enum = enum
  418. self.pattern = pattern
  419. self.items_ = items
  420. self.read_only = read_only
  421. self.default = default
  422. self._insert_extras__()
  423. if (properties or (additional_properties is not None)) and type != TYPE_OBJECT:
  424. raise AssertionError("only object Schema can have properties")
  425. _check_type(type, format, enum, pattern, items, self.__class__)
  426. def _remove_read_only(self):
  427. # readOnly is only valid for Schemas inside another Schema's properties;
  428. # when placing Schema elsewhere we must take care to remove the readOnly flag
  429. self.pop('readOnly', '')
  430. class _Ref(SwaggerDict):
  431. ref_name_re = re.compile(r"#/(?P<scope>.+)/(?P<name>[^/]+)$")
  432. def __init__(self, resolver, name, scope, expected_type, ignore_unresolved=False):
  433. """Base class for all reference types. A reference object has only one property, ``$ref``, which must be a JSON
  434. reference to a valid object in the specification, e.g. ``#/definitions/Article`` to refer to an article model.
  435. :param .ReferenceResolver resolver: component resolver which must contain the referenced object
  436. :param str name: referenced object name, e.g. "Article"
  437. :param str scope: reference scope, e.g. "definitions"
  438. :param type[.SwaggerDict] expected_type: the expected type that will be asserted on the object found in resolver
  439. :param bool ignore_unresolved: do not throw if the referenced object does not exist
  440. """
  441. super(_Ref, self).__init__()
  442. assert not type(self) == _Ref, "do not instantiate _Ref directly"
  443. ref_name = "#/{scope}/{name}".format(scope=scope, name=name)
  444. if not ignore_unresolved:
  445. obj = resolver.get(name, scope)
  446. assert isinstance(obj, expected_type), ref_name + " is a {actual}, not a {expected}" \
  447. .format(actual=type(obj).__name__, expected=expected_type.__name__)
  448. self.ref = ref_name
  449. def resolve(self, resolver):
  450. """Get the object targeted by this reference from the given component resolver.
  451. :param .ReferenceResolver resolver: component resolver which must contain the referenced object
  452. :returns: the target object
  453. """
  454. ref_match = self.ref_name_re.match(self.ref)
  455. return resolver.getdefault(ref_match.group('name'), scope=ref_match.group('scope'))
  456. def __setitem__(self, key, value):
  457. if key == "$ref":
  458. return super(_Ref, self).__setitem__(key, value)
  459. raise NotImplementedError("only $ref can be set on Reference objects (not %s)" % key)
  460. def __delitem__(self, key):
  461. raise NotImplementedError("cannot delete property of Reference object")
  462. class SchemaRef(_Ref):
  463. def __init__(self, resolver, schema_name, ignore_unresolved=False):
  464. """Adds a reference to a named Schema defined in the ``#/definitions/`` object.
  465. :param .ReferenceResolver resolver: component resolver which must contain the definition
  466. :param str schema_name: schema name
  467. :param bool ignore_unresolved: do not throw if the referenced object does not exist
  468. """
  469. assert SCHEMA_DEFINITIONS in resolver.scopes
  470. super(SchemaRef, self).__init__(resolver, schema_name, SCHEMA_DEFINITIONS, Schema, ignore_unresolved)
  471. Schema.OR_REF = (Schema, SchemaRef)
  472. def resolve_ref(ref_or_obj, resolver):
  473. """Resolve `ref_or_obj` if it is a reference type. Return it unchanged if not.
  474. :param ref_or_obj: object to dereference
  475. :type ref_or_obj: SwaggerDict or _Ref
  476. :param resolver: component resolver which must contain the referenced object
  477. """
  478. if isinstance(ref_or_obj, _Ref):
  479. return ref_or_obj.resolve(resolver)
  480. return ref_or_obj
  481. class Responses(SwaggerDict):
  482. def __init__(self, responses, default=None, **extra):
  483. """Describes the expected responses of an :class:`.Operation`.
  484. :param responses: mapping of status code to response definition
  485. :type responses: dict[str or int,Response]
  486. :param Response default: description of the response structure to expect if another status code is returned
  487. """
  488. super(Responses, self).__init__(**extra)
  489. for status, response in responses.items():
  490. if response is not None: # pragma: no cover
  491. self[str(status)] = response
  492. self.default = default
  493. self._insert_extras__()
  494. class Response(SwaggerDict):
  495. def __init__(self, description, schema=None, examples=None, **extra):
  496. """Describes the structure of an operation's response.
  497. :param str description: response description
  498. :param schema: structure of the response body
  499. :type schema: Schema or SchemaRef or rest_framework.serializers.Serializer
  500. or type[rest_framework.serializers.Serializer]
  501. :param dict examples: example bodies mapped by mime type
  502. """
  503. super(Response, self).__init__(**extra)
  504. self.description = description
  505. self.schema = schema
  506. self.examples = examples
  507. self._insert_extras__()
  508. if schema and isinstance(schema, Schema):
  509. schema._remove_read_only()
  510. class ReferenceResolver(object):
  511. """A mapping type intended for storing objects pointed at by Swagger Refs.
  512. Provides support and checks for different reference scopes, e.g. 'definitions'.
  513. For example:
  514. ::
  515. > components = ReferenceResolver('definitions', 'parameters')
  516. > definitions = components.with_scope('definitions')
  517. > definitions.set('Article', Schema(...))
  518. > print(components)
  519. {'definitions': OrderedDict([('Article', Schema(...)]), 'parameters': OrderedDict()}
  520. """
  521. def __init__(self, *scopes, **kwargs):
  522. """
  523. :param str scopes: an enumeration of the valid scopes this resolver will contain
  524. """
  525. force_init = kwargs.pop('force_init', False)
  526. if not force_init:
  527. raise AssertionError(
  528. "Creating an instance of ReferenceResolver almost certainly won't do what you want it to do.\n"
  529. "See https://github.com/axnsan12/drf-yasg/issues/211, "
  530. "https://github.com/axnsan12/drf-yasg/issues/271, "
  531. "https://github.com/axnsan12/drf-yasg/issues/325.\n"
  532. "Pass `force_init=True` to override this."
  533. )
  534. self._objects = OrderedDict()
  535. self._force_scope = None
  536. for scope in scopes:
  537. assert isinstance(scope, str), "scope names must be strings"
  538. self._objects[scope] = OrderedDict()
  539. def with_scope(self, scope):
  540. """Return a view into this :class:`.ReferenceResolver` whose scope is defaulted and forced to `scope`.
  541. :param str scope: target scope, must be in this resolver's `scopes`
  542. :return: the bound resolver
  543. :rtype: .ReferenceResolver
  544. """
  545. assert scope in self.scopes, "unknown scope %s" % scope
  546. ret = ReferenceResolver(force_init=True)
  547. ret._objects = self._objects
  548. ret._force_scope = scope
  549. return ret
  550. def _check_scope(self, scope):
  551. real_scope = self._force_scope or scope
  552. if scope is not None:
  553. assert not self._force_scope or scope == self._force_scope, "cannot override forced scope"
  554. assert real_scope and real_scope in self._objects, "invalid scope %s" % scope
  555. return real_scope
  556. def set(self, name, obj, scope=None):
  557. """Set an object in the given scope, raise an error if it already exists.
  558. :param str name: reference name
  559. :param obj: referenced object
  560. :param str scope: reference scope
  561. """
  562. scope = self._check_scope(scope)
  563. assert obj is not None, "referenced objects cannot be None/null"
  564. assert name not in self._objects[scope], "#/%s/%s already exists" % (scope, name)
  565. self._objects[scope][name] = obj
  566. def setdefault(self, name, maker, scope=None):
  567. """Set an object in the given scope only if it does not exist.
  568. :param str name: reference name
  569. :param function maker: object factory, called only if necessary
  570. :param str scope: reference scope
  571. """
  572. scope = self._check_scope(scope)
  573. assert callable(maker), "setdefault expects a callable, not %s" % type(maker).__name__
  574. ret = self.getdefault(name, None, scope)
  575. if ret is None:
  576. ret = maker()
  577. value = self.getdefault(name, None, scope)
  578. assert ret is not None, "maker returned None; referenced objects cannot be None/null"
  579. if value is None:
  580. self.set(name, ret, scope)
  581. elif value != ret:
  582. logger.debug("during setdefault, maker for %s inserted a value and returned a different value", name)
  583. ret = value
  584. return ret
  585. def get(self, name, scope=None):
  586. """Get an object from the given scope, raise an error if it does not exist.
  587. :param str name: reference name
  588. :param str scope: reference scope
  589. :return: the object
  590. """
  591. scope = self._check_scope(scope)
  592. assert name in self._objects[scope], "#/%s/%s is not defined" % (scope, name)
  593. return self._objects[scope][name]
  594. def getdefault(self, name, default=None, scope=None):
  595. """Get an object from the given scope or a default value if it does not exist.
  596. :param str name: reference name
  597. :param default: the default value
  598. :param str scope: reference scope
  599. :return: the object or `default`
  600. """
  601. scope = self._check_scope(scope)
  602. return self._objects[scope].get(name, default)
  603. def has(self, name, scope=None):
  604. """Check if an object exists in the given scope.
  605. :param str name: reference name
  606. :param str scope: reference scope
  607. :return: True if the object exists
  608. :rtype: bool
  609. """
  610. scope = self._check_scope(scope)
  611. return name in self._objects[scope]
  612. def __iter__(self):
  613. if self._force_scope:
  614. return iter(self._objects[self._force_scope])
  615. return iter(self._objects)
  616. @property
  617. def scopes(self):
  618. if self._force_scope:
  619. return [self._force_scope]
  620. return list(self._objects.keys())
  621. # act as mapping
  622. def keys(self):
  623. if self._force_scope:
  624. return self._objects[self._force_scope].keys()
  625. return self._objects.keys()
  626. def __getitem__(self, item):
  627. if self._force_scope:
  628. return self._objects[self._force_scope][item]
  629. return self._objects[item]
  630. def __str__(self):
  631. return str(dict(self))