utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. from coreapi import exceptions
  2. from coreapi.compat import string_types, text_type, urlparse, _TemporaryFileWrapper
  3. from collections import namedtuple
  4. import os
  5. import pkg_resources
  6. import tempfile
  7. def domain_matches(request, domain):
  8. """
  9. Domain string matching against an outgoing request.
  10. Patterns starting with '*' indicate a wildcard domain.
  11. """
  12. if (domain is None) or (domain == '*'):
  13. return True
  14. host = urlparse.urlparse(request.url).hostname
  15. if domain.startswith('*'):
  16. return host.endswith(domain[1:])
  17. return host == domain
  18. def get_installed_codecs():
  19. packages = [
  20. (package, package.load()) for package in
  21. pkg_resources.iter_entry_points(group='coreapi.codecs')
  22. ]
  23. return {
  24. package.name: cls() for (package, cls) in packages
  25. }
  26. # File utilities for upload and download support.
  27. File = namedtuple('File', 'name content content_type')
  28. File.__new__.__defaults__ = (None,)
  29. def is_file(obj):
  30. if isinstance(obj, File):
  31. return True
  32. if hasattr(obj, '__iter__') and not isinstance(obj, (string_types, list, tuple, dict)):
  33. # A stream object.
  34. return True
  35. return False
  36. def guess_filename(obj):
  37. name = getattr(obj, 'name', None)
  38. if name and isinstance(name, string_types) and name[0] != '<' and name[-1] != '>':
  39. return os.path.basename(name)
  40. return None
  41. def guess_extension(content_type):
  42. """
  43. Python's `mimetypes.guess_extension` is no use because it simply returns
  44. the first of an unordered set. We use the same set of media types here,
  45. but take a reasonable preference on what extension to map to.
  46. """
  47. return {
  48. 'application/javascript': '.js',
  49. 'application/msword': '.doc',
  50. 'application/octet-stream': '.bin',
  51. 'application/oda': '.oda',
  52. 'application/pdf': '.pdf',
  53. 'application/pkcs7-mime': '.p7c',
  54. 'application/postscript': '.ps',
  55. 'application/vnd.apple.mpegurl': '.m3u',
  56. 'application/vnd.ms-excel': '.xls',
  57. 'application/vnd.ms-powerpoint': '.ppt',
  58. 'application/x-bcpio': '.bcpio',
  59. 'application/x-cpio': '.cpio',
  60. 'application/x-csh': '.csh',
  61. 'application/x-dvi': '.dvi',
  62. 'application/x-gtar': '.gtar',
  63. 'application/x-hdf': '.hdf',
  64. 'application/x-latex': '.latex',
  65. 'application/x-mif': '.mif',
  66. 'application/x-netcdf': '.nc',
  67. 'application/x-pkcs12': '.p12',
  68. 'application/x-pn-realaudio': '.ram',
  69. 'application/x-python-code': '.pyc',
  70. 'application/x-sh': '.sh',
  71. 'application/x-shar': '.shar',
  72. 'application/x-shockwave-flash': '.swf',
  73. 'application/x-sv4cpio': '.sv4cpio',
  74. 'application/x-sv4crc': '.sv4crc',
  75. 'application/x-tar': '.tar',
  76. 'application/x-tcl': '.tcl',
  77. 'application/x-tex': '.tex',
  78. 'application/x-texinfo': '.texinfo',
  79. 'application/x-troff': '.tr',
  80. 'application/x-troff-man': '.man',
  81. 'application/x-troff-me': '.me',
  82. 'application/x-troff-ms': '.ms',
  83. 'application/x-ustar': '.ustar',
  84. 'application/x-wais-source': '.src',
  85. 'application/xml': '.xml',
  86. 'application/zip': '.zip',
  87. 'audio/basic': '.au',
  88. 'audio/mpeg': '.mp3',
  89. 'audio/x-aiff': '.aif',
  90. 'audio/x-pn-realaudio': '.ra',
  91. 'audio/x-wav': '.wav',
  92. 'image/gif': '.gif',
  93. 'image/ief': '.ief',
  94. 'image/jpeg': '.jpe',
  95. 'image/png': '.png',
  96. 'image/svg+xml': '.svg',
  97. 'image/tiff': '.tiff',
  98. 'image/vnd.microsoft.icon': '.ico',
  99. 'image/x-cmu-raster': '.ras',
  100. 'image/x-ms-bmp': '.bmp',
  101. 'image/x-portable-anymap': '.pnm',
  102. 'image/x-portable-bitmap': '.pbm',
  103. 'image/x-portable-graymap': '.pgm',
  104. 'image/x-portable-pixmap': '.ppm',
  105. 'image/x-rgb': '.rgb',
  106. 'image/x-xbitmap': '.xbm',
  107. 'image/x-xpixmap': '.xpm',
  108. 'image/x-xwindowdump': '.xwd',
  109. 'message/rfc822': '.eml',
  110. 'text/css': '.css',
  111. 'text/csv': '.csv',
  112. 'text/html': '.html',
  113. 'text/plain': '.txt',
  114. 'text/richtext': '.rtx',
  115. 'text/tab-separated-values': '.tsv',
  116. 'text/x-python': '.py',
  117. 'text/x-setext': '.etx',
  118. 'text/x-sgml': '.sgml',
  119. 'text/x-vcard': '.vcf',
  120. 'text/xml': '.xml',
  121. 'video/mp4': '.mp4',
  122. 'video/mpeg': '.mpeg',
  123. 'video/quicktime': '.mov',
  124. 'video/webm': '.webm',
  125. 'video/x-msvideo': '.avi',
  126. 'video/x-sgi-movie': '.movie'
  127. }.get(content_type, '')
  128. if _TemporaryFileWrapper:
  129. # Ideally we subclass this so that we can present a custom representation.
  130. class DownloadedFile(_TemporaryFileWrapper):
  131. basename = None
  132. def __repr__(self):
  133. state = "closed" if self.closed else "open"
  134. mode = "" if self.closed else " '%s'" % self.file.mode
  135. return "<DownloadedFile '%s', %s%s>" % (self.name, state, mode)
  136. def __str__(self):
  137. return self.__repr__()
  138. else:
  139. # On some platforms (eg GAE) the private _TemporaryFileWrapper may not be
  140. # available, just use the standard `NamedTemporaryFile` function
  141. # in this case.
  142. DownloadedFile = tempfile.NamedTemporaryFile
  143. # Negotiation utilities. USed to determine which codec or transport class
  144. # should be used, given a list of supported instances.
  145. def determine_transport(transports, url):
  146. """
  147. Given a URL determine the appropriate transport instance.
  148. """
  149. url_components = urlparse.urlparse(url)
  150. scheme = url_components.scheme.lower()
  151. netloc = url_components.netloc
  152. if not scheme:
  153. raise exceptions.NetworkError("URL missing scheme '%s'." % url)
  154. if not netloc:
  155. raise exceptions.NetworkError("URL missing hostname '%s'." % url)
  156. for transport in transports:
  157. if scheme in transport.schemes:
  158. return transport
  159. raise exceptions.NetworkError("Unsupported URL scheme '%s'." % scheme)
  160. def negotiate_decoder(decoders, content_type=None):
  161. """
  162. Given the value of a 'Content-Type' header, return the appropriate
  163. codec for decoding the request content.
  164. """
  165. if content_type is None:
  166. return decoders[0]
  167. content_type = content_type.split(';')[0].strip().lower()
  168. main_type = content_type.split('/')[0] + '/*'
  169. wildcard_type = '*/*'
  170. for codec in decoders:
  171. for media_type in codec.get_media_types():
  172. if media_type in (content_type, main_type, wildcard_type):
  173. return codec
  174. msg = "Unsupported media in Content-Type header '%s'" % content_type
  175. raise exceptions.NoCodecAvailable(msg)
  176. def negotiate_encoder(encoders, accept=None):
  177. """
  178. Given the value of a 'Accept' header, return the appropriate codec for
  179. encoding the response content.
  180. """
  181. if accept is None:
  182. return encoders[0]
  183. acceptable = set([
  184. item.split(';')[0].strip().lower()
  185. for item in accept.split(',')
  186. ])
  187. for codec in encoders:
  188. for media_type in codec.get_media_types():
  189. if media_type in acceptable:
  190. return codec
  191. for codec in encoders:
  192. for media_type in codec.get_media_types():
  193. if codec.media_type.split('/')[0] + '/*' in acceptable:
  194. return codec
  195. if '*/*' in acceptable:
  196. return encoders[0]
  197. msg = "Unsupported media in Accept header '%s'" % accept
  198. raise exceptions.NoCodecAvailable(msg)
  199. # Validation utilities. Used to ensure that we get consistent validation
  200. # exceptions when invalid types are passed as a parameter, rather than
  201. # an exception occuring when the request is made.
  202. def validate_path_param(value):
  203. value = _validate_form_field(value, allow_list=False)
  204. if not value:
  205. msg = 'Parameter %s: May not be empty.'
  206. raise exceptions.ParameterError(msg)
  207. return value
  208. def validate_query_param(value):
  209. return _validate_form_field(value)
  210. def validate_body_param(value, encoding):
  211. if encoding == 'application/json':
  212. return _validate_json_data(value)
  213. elif encoding == 'multipart/form-data':
  214. return _validate_form_object(value, allow_files=True)
  215. elif encoding == 'application/x-www-form-urlencoded':
  216. return _validate_form_object(value)
  217. elif encoding == 'application/octet-stream':
  218. if not is_file(value):
  219. msg = 'Must be an file upload.'
  220. raise exceptions.ParameterError(msg)
  221. return value
  222. msg = 'Unsupported encoding "%s" for outgoing request.'
  223. raise exceptions.NetworkError(msg % encoding)
  224. def validate_form_param(value, encoding):
  225. if encoding == 'application/json':
  226. return _validate_json_data(value)
  227. elif encoding == 'multipart/form-data':
  228. return _validate_form_field(value, allow_files=True)
  229. elif encoding == 'application/x-www-form-urlencoded':
  230. return _validate_form_field(value)
  231. msg = 'Unsupported encoding "%s" for outgoing request.'
  232. raise exceptions.NetworkError(msg % encoding)
  233. def _validate_form_object(value, allow_files=False):
  234. """
  235. Ensure that `value` can be encoded as form data or as query parameters.
  236. """
  237. if not isinstance(value, dict):
  238. msg = 'Must be an object.'
  239. raise exceptions.ParameterError(msg)
  240. return {
  241. text_type(item_key): _validate_form_field(item_val, allow_files=allow_files)
  242. for item_key, item_val in value.items()
  243. }
  244. def _validate_form_field(value, allow_files=False, allow_list=True):
  245. """
  246. Ensure that `value` can be encoded as a single form data or a query parameter.
  247. Basic types that has a simple string representation are supported.
  248. A list of basic types is also valid.
  249. """
  250. if isinstance(value, string_types):
  251. return value
  252. elif isinstance(value, bool) or (value is None):
  253. return {True: 'true', False: 'false', None: ''}[value]
  254. elif isinstance(value, (int, float)):
  255. return "%s" % value
  256. elif allow_list and isinstance(value, (list, tuple)) and not is_file(value):
  257. # Only the top-level element may be a list.
  258. return [
  259. _validate_form_field(item, allow_files=False, allow_list=False)
  260. for item in value
  261. ]
  262. elif allow_files and is_file(value):
  263. return value
  264. msg = 'Must be a primitive type.'
  265. raise exceptions.ParameterError(msg)
  266. def _validate_json_data(value):
  267. """
  268. Ensure that `value` can be encoded into JSON.
  269. """
  270. if (value is None) or isinstance(value, (bool, int, float, string_types)):
  271. return value
  272. elif isinstance(value, (list, tuple)) and not is_file(value):
  273. return [_validate_json_data(item) for item in value]
  274. elif isinstance(value, dict):
  275. return {
  276. text_type(item_key): _validate_json_data(item_val)
  277. for item_key, item_val in value.items()
  278. }
  279. msg = 'Must be a JSON primitive.'
  280. raise exceptions.ParameterError(msg)