You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

175 lines
7.0 KiB

  1. """HTTP client library"""
  2. import nilmdb.utils
  3. from nilmdb.client.errors import ClientError, ServerError, Error
  4. import simplejson as json
  5. import urlparse
  6. import requests
  7. class HTTPClient(object):
  8. """Class to manage and perform HTTP requests from the client"""
  9. def __init__(self, baseurl = "", post_json = False, verify_ssl = True):
  10. """If baseurl is supplied, all other functions that take
  11. a URL can be given a relative URL instead."""
  12. # Verify / clean up URL
  13. reparsed = urlparse.urlparse(baseurl).geturl()
  14. if '://' not in reparsed:
  15. reparsed = urlparse.urlparse("http://" + baseurl).geturl()
  16. self.baseurl = reparsed.rstrip('/') + '/'
  17. # Build Requests session object, enable SSL verification
  18. self.verify_ssl = verify_ssl
  19. self.session = requests.Session()
  20. self.session.verify = True
  21. # Saved response, so that tests can verify a few things.
  22. self._last_response = {}
  23. # Whether to send application/json POST bodies (versus
  24. # x-www-form-urlencoded)
  25. self.post_json = post_json
  26. def _handle_error(self, url, code, body):
  27. # Default variables for exception. We use the entire body as
  28. # the default message, in case we can't extract it from a JSON
  29. # response.
  30. args = { "url" : url,
  31. "status" : str(code),
  32. "message" : body,
  33. "traceback" : None }
  34. try:
  35. # Fill with server-provided data if we can
  36. jsonerror = json.loads(body)
  37. args["status"] = jsonerror["status"]
  38. args["message"] = jsonerror["message"]
  39. args["traceback"] = jsonerror["traceback"]
  40. except Exception: # pragma: no cover
  41. pass
  42. if code >= 400 and code <= 499:
  43. raise ClientError(**args)
  44. else: # pragma: no cover
  45. if code >= 500 and code <= 599:
  46. if args["message"] is None:
  47. args["message"] = ("(no message; try disabling " +
  48. "response.stream option in " +
  49. "nilmdb.server for better debugging)")
  50. raise ServerError(**args)
  51. else:
  52. raise Error(**args)
  53. def close(self):
  54. self.session.close()
  55. def _do_req(self, method, url, query_data, body_data, stream, headers):
  56. url = urlparse.urljoin(self.baseurl, url)
  57. try:
  58. response = self.session.request(method, url,
  59. params = query_data,
  60. data = body_data,
  61. stream = stream,
  62. headers = headers,
  63. verify = self.verify_ssl)
  64. except requests.RequestException as e:
  65. raise ServerError(status = "502 Error", url = url,
  66. message = str(e.message))
  67. if response.status_code != 200:
  68. self._handle_error(url, response.status_code, response.content)
  69. self._last_response = response
  70. if response.headers["content-type"] in ("application/json",
  71. "application/x-json-stream"):
  72. return (response, True)
  73. else:
  74. return (response, False)
  75. # Normal versions that return data directly
  76. def _req(self, method, url, query = None, body = None, headers = None):
  77. """
  78. Make a request and return the body data as a string or parsed
  79. JSON object, or raise an error if it contained an error.
  80. """
  81. (response, isjson) = self._do_req(method, url, query, body,
  82. stream = False, headers = headers)
  83. if isjson:
  84. return json.loads(response.content)
  85. return response.content
  86. def get(self, url, params = None):
  87. """Simple GET (parameters in URL)"""
  88. return self._req("GET", url, params, None)
  89. def post(self, url, params = None):
  90. """Simple POST (parameters in body)"""
  91. if self.post_json:
  92. return self._req("POST", url, None,
  93. json.dumps(params),
  94. { 'Content-type': 'application/json' })
  95. else:
  96. return self._req("POST", url, None, params)
  97. def put(self, url, data, params = None, binary = False):
  98. """Simple PUT (parameters in URL, data in body)"""
  99. if binary:
  100. h = { 'Content-type': 'application/octet-stream' }
  101. else:
  102. h = { 'Content-type': 'text/plain; charset=utf-8' }
  103. return self._req("PUT", url, query = params, body = data, headers = h)
  104. # Generator versions that return data one line at a time.
  105. def _req_gen(self, method, url, query = None, body = None,
  106. headers = None, binary = False):
  107. """
  108. Make a request and return a generator that gives back strings
  109. or JSON decoded lines of the body data, or raise an error if
  110. it contained an eror.
  111. """
  112. (response, isjson) = self._do_req(method, url, query, body,
  113. stream = True, headers = headers)
  114. # Like the iter_lines function in Requests, but only splits on
  115. # the specified line ending.
  116. def lines(source, ending):
  117. pending = None
  118. for chunk in source:
  119. if pending is not None:
  120. chunk = pending + chunk
  121. tmp = chunk.split(ending)
  122. lines = tmp[:-1]
  123. if chunk.endswith(ending):
  124. pending = None
  125. else:
  126. pending = tmp[-1]
  127. for line in lines:
  128. yield line
  129. if pending is not None: # pragma: no cover (missing newline)
  130. yield pending
  131. # Yield the chunks or lines as requested
  132. if binary:
  133. for chunk in response.iter_content(chunk_size = 65536):
  134. yield chunk
  135. elif isjson:
  136. for line in lines(response.iter_content(chunk_size = 1),
  137. ending = '\r\n'):
  138. yield json.loads(line)
  139. else:
  140. for line in lines(response.iter_content(chunk_size = 65536),
  141. ending = '\n'):
  142. yield line
  143. def get_gen(self, url, params = None, binary = False):
  144. """Simple GET (parameters in URL) returning a generator"""
  145. return self._req_gen("GET", url, params, binary = binary)
  146. def post_gen(self, url, params = None):
  147. """Simple POST (parameters in body) returning a generator"""
  148. if self.post_json:
  149. return self._req_gen("POST", url, None,
  150. json.dumps(params),
  151. { 'Content-type': 'application/json' })
  152. else:
  153. return self._req_gen("POST", url, None, params)
  154. # Not much use for a POST or PUT generator, since they don't
  155. # return much data.