client.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from coreapi import codecs, exceptions, transports
  2. from coreapi.compat import string_types
  3. from coreapi.document import Document, Link
  4. from coreapi.utils import determine_transport, get_installed_codecs
  5. import collections
  6. import itypes
  7. LinkAncestor = collections.namedtuple('LinkAncestor', ['document', 'keys'])
  8. def _lookup_link(document, keys):
  9. """
  10. Validates that keys looking up a link are correct.
  11. Returns a two-tuple of (link, link_ancestors).
  12. """
  13. if not isinstance(keys, (list, tuple)):
  14. msg = "'keys' must be a list of strings or ints."
  15. raise TypeError(msg)
  16. if any([
  17. not isinstance(key, string_types) and not isinstance(key, int)
  18. for key in keys
  19. ]):
  20. raise TypeError("'keys' must be a list of strings or ints.")
  21. # Determine the link node being acted on, and its parent document.
  22. # 'node' is the link we're calling the action for.
  23. # 'document_keys' is the list of keys to the link's parent document.
  24. node = document
  25. link_ancestors = [LinkAncestor(document=document, keys=[])]
  26. for idx, key in enumerate(keys):
  27. try:
  28. node = node[key]
  29. except (KeyError, IndexError, TypeError):
  30. index_string = ''.join('[%s]' % repr(key).strip('u') for key in keys)
  31. msg = 'Index %s did not reference a link. Key %s was not found.'
  32. raise exceptions.LinkLookupError(msg % (index_string, repr(key).strip('u')))
  33. if isinstance(node, Document):
  34. ancestor = LinkAncestor(document=node, keys=keys[:idx + 1])
  35. link_ancestors.append(ancestor)
  36. # Ensure that we've correctly indexed into a link.
  37. if not isinstance(node, Link):
  38. index_string = ''.join('[%s]' % repr(key).strip('u') for key in keys)
  39. msg = "Can only call 'action' on a Link. Index %s returned type '%s'."
  40. raise exceptions.LinkLookupError(
  41. msg % (index_string, type(node).__name__)
  42. )
  43. return (node, link_ancestors)
  44. def _validate_parameters(link, parameters):
  45. """
  46. Ensure that parameters passed to the link are correct.
  47. Raises a `ParameterError` if any parameters do not validate.
  48. """
  49. provided = set(parameters.keys())
  50. required = set([
  51. field.name for field in link.fields if field.required
  52. ])
  53. optional = set([
  54. field.name for field in link.fields if not field.required
  55. ])
  56. errors = {}
  57. # Determine if any required field names not supplied.
  58. missing = required - provided
  59. for item in missing:
  60. errors[item] = 'This parameter is required.'
  61. # Determine any parameter names supplied that are not valid.
  62. unexpected = provided - (optional | required)
  63. for item in unexpected:
  64. errors[item] = 'Unknown parameter.'
  65. if errors:
  66. raise exceptions.ParameterError(errors)
  67. def get_default_decoders():
  68. return [
  69. codecs.CoreJSONCodec(),
  70. codecs.JSONCodec(),
  71. codecs.TextCodec(),
  72. codecs.DownloadCodec()
  73. ]
  74. def get_default_transports(auth=None, session=None):
  75. return [
  76. transports.HTTPTransport(auth=auth, session=session)
  77. ]
  78. class Client(itypes.Object):
  79. def __init__(self, decoders=None, transports=None, auth=None, session=None):
  80. assert transports is None or auth is None, (
  81. "Cannot specify both 'auth' and 'transports'. "
  82. "When specifying transport instances explicitly you should set "
  83. "the authentication directly on the transport."
  84. )
  85. if decoders is None:
  86. decoders = get_default_decoders()
  87. if transports is None:
  88. transports = get_default_transports(auth=auth)
  89. self._decoders = itypes.List(decoders)
  90. self._transports = itypes.List(transports)
  91. @property
  92. def decoders(self):
  93. return self._decoders
  94. @property
  95. def transports(self):
  96. return self._transports
  97. def get(self, url, format=None, force_codec=False):
  98. link = Link(url, action='get')
  99. decoders = self.decoders
  100. if format:
  101. force_codec = True
  102. decoders = [decoder for decoder in self.decoders if decoder.format == format]
  103. if not decoders:
  104. installed_codecs = get_installed_codecs()
  105. if format in installed_codecs:
  106. decoders = [installed_codecs[format]]
  107. else:
  108. raise ValueError("No decoder available with format='%s'" % format)
  109. # Perform the action, and return a new document.
  110. transport = determine_transport(self.transports, link.url)
  111. return transport.transition(link, decoders, force_codec=force_codec)
  112. def reload(self, document, format=None, force_codec=False):
  113. # Fallback for v1.x. To be removed in favour of explict `get` style.
  114. return self.get(document.url, format=format, force_codec=force_codec)
  115. def action(self, document, keys, params=None, validate=True, overrides=None,
  116. action=None, encoding=None, transform=None):
  117. if (action is not None) or (encoding is not None) or (transform is not None):
  118. # Fallback for v1.x overrides.
  119. # Will be removed at some point, most likely in a 2.1 release.
  120. if overrides is None:
  121. overrides = {}
  122. if action is not None:
  123. overrides['action'] = action
  124. if encoding is not None:
  125. overrides['encoding'] = encoding
  126. if transform is not None:
  127. overrides['transform'] = transform
  128. if isinstance(keys, string_types):
  129. keys = [keys]
  130. if params is None:
  131. params = {}
  132. # Validate the keys and link parameters.
  133. link, link_ancestors = _lookup_link(document, keys)
  134. if validate:
  135. _validate_parameters(link, params)
  136. if overrides:
  137. # Handle any explicit overrides.
  138. url = overrides.get('url', link.url)
  139. action = overrides.get('action', link.action)
  140. encoding = overrides.get('encoding', link.encoding)
  141. transform = overrides.get('transform', link.transform)
  142. fields = overrides.get('fields', link.fields)
  143. link = Link(url, action=action, encoding=encoding, transform=transform, fields=fields)
  144. # Perform the action, and return a new document.
  145. transport = determine_transport(self.transports, link.url)
  146. return transport.transition(link, self.decoders, params=params, link_ancestors=link_ancestors)