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.
 
 
 

138 lines
5.4 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):
  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.