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.

httpclient.py 5.4 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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):
  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.session = requests.Session()
  19. self.session.verify = True
  20. # Saved response, so that tests can verify a few things.
  21. self._last_response = {}
  22. # Whether to send application/json POST bodies (versus
  23. # x-www-form-urlencoded)
  24. self.post_json = post_json
  25. def _handle_error(self, url, code, body):
  26. # Default variables for exception. We use the entire body as
  27. # the default message, in case we can't extract it from a JSON
  28. # response.
  29. args = { "url" : url,
  30. "status" : str(code),
  31. "message" : body,
  32. "traceback" : None }
  33. try:
  34. # Fill with server-provided data if we can
  35. jsonerror = json.loads(body)
  36. args["status"] = jsonerror["status"]
  37. args["message"] = jsonerror["message"]
  38. args["traceback"] = jsonerror["traceback"]
  39. except Exception: # pragma: no cover
  40. pass
  41. if code >= 400 and code <= 499:
  42. raise ClientError(**args)
  43. else: # pragma: no cover
  44. if code >= 500 and code <= 599:
  45. if args["message"] is None:
  46. args["message"] = ("(no message; try disabling " +
  47. "response.stream option in " +
  48. "nilmdb.server for better debugging)")
  49. raise ServerError(**args)
  50. else:
  51. raise Error(**args)
  52. def close(self):
  53. self.session.close()
  54. def _do_req(self, method, url, query_data, body_data, stream, headers):
  55. url = urlparse.urljoin(self.baseurl, url)
  56. try:
  57. response = self.session.request(method, url,
  58. params = query_data,
  59. data = body_data,
  60. stream = stream,
  61. headers = headers)
  62. except requests.RequestException as e:
  63. raise ServerError(status = "502 Error", url = url,
  64. message = str(e.message))
  65. if response.status_code != 200:
  66. self._handle_error(url, response.status_code, response.content)
  67. self._last_response = response
  68. if response.headers["content-type"] in ("application/json",
  69. "application/x-json-stream"):
  70. return (response, True)
  71. else:
  72. return (response, False)
  73. # Normal versions that return data directly
  74. def _req(self, method, url, query = None, body = None, headers = None):
  75. """
  76. Make a request and return the body data as a string or parsed
  77. JSON object, or raise an error if it contained an error.
  78. """
  79. (response, isjson) = self._do_req(method, url, query, body,
  80. stream = False, headers = headers)
  81. if isjson:
  82. return json.loads(response.content)
  83. return response.content
  84. def get(self, url, params = None):
  85. """Simple GET (parameters in URL)"""
  86. return self._req("GET", url, params, None)
  87. def post(self, url, params = None):
  88. """Simple POST (parameters in body)"""
  89. if self.post_json:
  90. return self._req("POST", url, None,
  91. json.dumps(params),
  92. { 'Content-type': 'application/json' })
  93. else:
  94. return self._req("POST", url, None, params)
  95. def put(self, url, data, params = None):
  96. """Simple PUT (parameters in URL, data in body)"""
  97. return self._req("PUT", url, params, data)
  98. # Generator versions that return data one line at a time.
  99. def _req_gen(self, method, url, query = None, body = None,
  100. headers = None, binary = False):
  101. """
  102. Make a request and return a generator that gives back strings
  103. or JSON decoded lines of the body data, or raise an error if
  104. it contained an eror.
  105. """
  106. (response, isjson) = self._do_req(method, url, query, body,
  107. stream = True, headers = headers)
  108. if binary:
  109. for chunk in response.iter_content(chunk_size = 65536):
  110. yield chunk
  111. elif isjson:
  112. for line in response.iter_lines():
  113. yield json.loads(line)
  114. else:
  115. for line in response.iter_lines():
  116. yield line
  117. def get_gen(self, url, params = None, binary = False):
  118. """Simple GET (parameters in URL) returning a generator"""
  119. return self._req_gen("GET", url, params, binary = binary)
  120. # Not much use for a POST or PUT generator, since they don't
  121. # return much data.