document.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. from collections import OrderedDict, namedtuple
  4. from coreapi.compat import string_types
  5. import itypes
  6. def _to_immutable(value):
  7. if isinstance(value, dict):
  8. return Object(value)
  9. elif isinstance(value, list):
  10. return Array(value)
  11. return value
  12. def _repr(node):
  13. from coreapi.codecs.python import PythonCodec
  14. return PythonCodec().encode(node)
  15. def _str(node):
  16. from coreapi.codecs.display import DisplayCodec
  17. return DisplayCodec().encode(node)
  18. def _key_sorting(item):
  19. """
  20. Document and Object sorting.
  21. Regular attributes sorted alphabetically.
  22. Links are sorted based on their URL and action.
  23. """
  24. key, value = item
  25. if isinstance(value, Link):
  26. action_priority = {
  27. 'get': 0,
  28. 'post': 1,
  29. 'put': 2,
  30. 'patch': 3,
  31. 'delete': 4
  32. }.get(value.action, 5)
  33. return (1, (value.url, action_priority))
  34. return (0, key)
  35. # The field class, as used by Link objects:
  36. # NOTE: 'type', 'description' and 'example' are now deprecated,
  37. # in favor of 'schema'.
  38. Field = namedtuple('Field', ['name', 'required', 'location', 'schema', 'description', 'type', 'example'])
  39. Field.__new__.__defaults__ = (False, '', None, None, None, None)
  40. # The Core API primitives:
  41. class Document(itypes.Dict):
  42. """
  43. The Core API document type.
  44. Expresses the data that the client may access,
  45. and the actions that the client may perform.
  46. """
  47. def __init__(self, url=None, title=None, description=None, media_type=None, content=None):
  48. content = {} if (content is None) else content
  49. if url is not None and not isinstance(url, string_types):
  50. raise TypeError("'url' must be a string.")
  51. if title is not None and not isinstance(title, string_types):
  52. raise TypeError("'title' must be a string.")
  53. if description is not None and not isinstance(description, string_types):
  54. raise TypeError("'description' must be a string.")
  55. if media_type is not None and not isinstance(media_type, string_types):
  56. raise TypeError("'media_type' must be a string.")
  57. if not isinstance(content, dict):
  58. raise TypeError("'content' must be a dict.")
  59. if any([not isinstance(key, string_types) for key in content.keys()]):
  60. raise TypeError('content keys must be strings.')
  61. self._url = '' if (url is None) else url
  62. self._title = '' if (title is None) else title
  63. self._description = '' if (description is None) else description
  64. self._media_type = '' if (media_type is None) else media_type
  65. self._data = {key: _to_immutable(value) for key, value in content.items()}
  66. def clone(self, data):
  67. return self.__class__(self.url, self.title, self.description, self.media_type, data)
  68. def __iter__(self):
  69. items = sorted(self._data.items(), key=_key_sorting)
  70. return iter([key for key, value in items])
  71. def __repr__(self):
  72. return _repr(self)
  73. def __str__(self):
  74. return _str(self)
  75. def __eq__(self, other):
  76. if self.__class__ == other.__class__:
  77. return (
  78. self.url == other.url and
  79. self.title == other.title and
  80. self._data == other._data
  81. )
  82. return super(Document, self).__eq__(other)
  83. @property
  84. def url(self):
  85. return self._url
  86. @property
  87. def title(self):
  88. return self._title
  89. @property
  90. def description(self):
  91. return self._description
  92. @property
  93. def media_type(self):
  94. return self._media_type
  95. @property
  96. def data(self):
  97. return OrderedDict([
  98. (key, value) for key, value in self.items()
  99. if not isinstance(value, Link)
  100. ])
  101. @property
  102. def links(self):
  103. return OrderedDict([
  104. (key, value) for key, value in self.items()
  105. if isinstance(value, Link)
  106. ])
  107. class Object(itypes.Dict):
  108. """
  109. An immutable mapping of strings to values.
  110. """
  111. def __init__(self, *args, **kwargs):
  112. data = dict(*args, **kwargs)
  113. if any([not isinstance(key, string_types) for key in data.keys()]):
  114. raise TypeError('Object keys must be strings.')
  115. self._data = {key: _to_immutable(value) for key, value in data.items()}
  116. def __iter__(self):
  117. items = sorted(self._data.items(), key=_key_sorting)
  118. return iter([key for key, value in items])
  119. def __repr__(self):
  120. return _repr(self)
  121. def __str__(self):
  122. return _str(self)
  123. @property
  124. def data(self):
  125. return OrderedDict([
  126. (key, value) for key, value in self.items()
  127. if not isinstance(value, Link)
  128. ])
  129. @property
  130. def links(self):
  131. return OrderedDict([
  132. (key, value) for key, value in self.items()
  133. if isinstance(value, Link)
  134. ])
  135. class Array(itypes.List):
  136. """
  137. An immutable list type container.
  138. """
  139. def __init__(self, *args):
  140. self._data = [_to_immutable(value) for value in list(*args)]
  141. def __repr__(self):
  142. return _repr(self)
  143. def __str__(self):
  144. return _str(self)
  145. class Link(itypes.Object):
  146. """
  147. Links represent the actions that a client may perform.
  148. """
  149. def __init__(self, url=None, action=None, encoding=None, transform=None, title=None, description=None, fields=None):
  150. if (url is not None) and (not isinstance(url, string_types)):
  151. raise TypeError("Argument 'url' must be a string.")
  152. if (action is not None) and (not isinstance(action, string_types)):
  153. raise TypeError("Argument 'action' must be a string.")
  154. if (encoding is not None) and (not isinstance(encoding, string_types)):
  155. raise TypeError("Argument 'encoding' must be a string.")
  156. if (transform is not None) and (not isinstance(transform, string_types)):
  157. raise TypeError("Argument 'transform' must be a string.")
  158. if (title is not None) and (not isinstance(title, string_types)):
  159. raise TypeError("Argument 'title' must be a string.")
  160. if (description is not None) and (not isinstance(description, string_types)):
  161. raise TypeError("Argument 'description' must be a string.")
  162. if (fields is not None) and (not isinstance(fields, (list, tuple))):
  163. raise TypeError("Argument 'fields' must be a list.")
  164. if (fields is not None) and any([
  165. not (isinstance(item, string_types) or isinstance(item, Field))
  166. for item in fields
  167. ]):
  168. raise TypeError("Argument 'fields' must be a list of strings or fields.")
  169. self._url = '' if (url is None) else url
  170. self._action = '' if (action is None) else action
  171. self._encoding = '' if (encoding is None) else encoding
  172. self._transform = '' if (transform is None) else transform
  173. self._title = '' if (title is None) else title
  174. self._description = '' if (description is None) else description
  175. self._fields = () if (fields is None) else tuple([
  176. item if isinstance(item, Field) else Field(item, required=False, location='')
  177. for item in fields
  178. ])
  179. @property
  180. def url(self):
  181. return self._url
  182. @property
  183. def action(self):
  184. return self._action
  185. @property
  186. def encoding(self):
  187. return self._encoding
  188. @property
  189. def transform(self):
  190. return self._transform
  191. @property
  192. def title(self):
  193. return self._title
  194. @property
  195. def description(self):
  196. return self._description
  197. @property
  198. def fields(self):
  199. return self._fields
  200. def __eq__(self, other):
  201. return (
  202. isinstance(other, Link) and
  203. self.url == other.url and
  204. self.action == other.action and
  205. self.encoding == other.encoding and
  206. self.transform == other.transform and
  207. self.description == other.description and
  208. sorted(self.fields, key=lambda f: f.name) == sorted(other.fields, key=lambda f: f.name)
  209. )
  210. def __repr__(self):
  211. return _repr(self)
  212. def __str__(self):
  213. return _str(self)
  214. class Error(itypes.Dict):
  215. def __init__(self, title=None, content=None):
  216. data = {} if (content is None) else content
  217. if title is not None and not isinstance(title, string_types):
  218. raise TypeError("'title' must be a string.")
  219. if content is not None and not isinstance(content, dict):
  220. raise TypeError("'content' must be a dict.")
  221. if any([not isinstance(key, string_types) for key in data.keys()]):
  222. raise TypeError('content keys must be strings.')
  223. self._title = '' if (title is None) else title
  224. self._data = {key: _to_immutable(value) for key, value in data.items()}
  225. def __iter__(self):
  226. items = sorted(self._data.items(), key=_key_sorting)
  227. return iter([key for key, value in items])
  228. def __repr__(self):
  229. return _repr(self)
  230. def __str__(self):
  231. return _str(self)
  232. def __eq__(self, other):
  233. return (
  234. isinstance(other, Error) and
  235. self.title == other.title and
  236. self._data == other._data
  237. )
  238. @property
  239. def title(self):
  240. return self._title
  241. def get_messages(self):
  242. messages = []
  243. for value in self.values():
  244. if isinstance(value, Array):
  245. messages += [
  246. item for item in value if isinstance(item, string_types)
  247. ]
  248. return messages