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.
 
 
 

197 lines
7.1 KiB

  1. """HTTP client library"""
  2. import nilmdb
  3. import nilmdb.utils
  4. from nilmdb.client.errors import ClientError, ServerError, Error
  5. import simplejson as json
  6. import urlparse
  7. import pycurl
  8. import cStringIO
  9. class HTTPClient(object):
  10. """Class to manage and perform HTTP requests from the client"""
  11. def __init__(self, baseurl = ""):
  12. """If baseurl is supplied, all other functions that take
  13. a URL can be given a relative URL instead."""
  14. # Verify / clean up URL
  15. reparsed = urlparse.urlparse(baseurl).geturl()
  16. if '://' not in reparsed:
  17. reparsed = urlparse.urlparse("http://" + baseurl).geturl()
  18. self.baseurl = reparsed
  19. self.curl = pycurl.Curl()
  20. self.curl.setopt(pycurl.SSL_VERIFYHOST, 2)
  21. self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
  22. self.curl.setopt(pycurl.MAXREDIRS, 5)
  23. self._setup_url()
  24. def _setup_url(self, url = "", params = ""):
  25. url = urlparse.urljoin(self.baseurl, url)
  26. if params:
  27. url = urlparse.urljoin(
  28. url, "?" + nilmdb.utils.urllib.urlencode(params))
  29. self.curl.setopt(pycurl.URL, url)
  30. self.url = url
  31. def _check_error(self, body = None):
  32. code = self.curl.getinfo(pycurl.RESPONSE_CODE)
  33. if code == 200:
  34. return
  35. # Default variables for exception. We use the entire body as
  36. # the default message, in case we can't extract it from a JSON
  37. # response.
  38. args = { "url" : self.url,
  39. "status" : str(code),
  40. "message" : body,
  41. "traceback" : None }
  42. try:
  43. # Fill with server-provided data if we can
  44. jsonerror = json.loads(body)
  45. args["status"] = jsonerror["status"]
  46. args["message"] = jsonerror["message"]
  47. args["traceback"] = jsonerror["traceback"]
  48. except Exception: # pragma: no cover
  49. pass
  50. if code >= 400 and code <= 499:
  51. raise ClientError(**args)
  52. else: # pragma: no cover
  53. if code >= 500 and code <= 599:
  54. if args["message"] is None:
  55. args["message"] = ("(no message; try disabling " +
  56. "response.stream option in " +
  57. "nilmdb.server for better debugging)")
  58. raise ServerError(**args)
  59. else:
  60. raise Error(**args)
  61. def _req_generator(self, url, params):
  62. """
  63. Like self._req(), but runs the perform in a separate thread.
  64. It returns a generator that spits out arbitrary-sized chunks
  65. of the resulting data, instead of using the WRITEFUNCTION
  66. callback.
  67. """
  68. self._setup_url(url, params)
  69. self._status = None
  70. error_body = ""
  71. self._headers = ""
  72. def header_callback(data):
  73. if self._status is None:
  74. self._status = int(data.split(" ")[1])
  75. self._headers += data
  76. self.curl.setopt(pycurl.HEADERFUNCTION, header_callback)
  77. def perform(callback):
  78. self.curl.setopt(pycurl.WRITEFUNCTION, callback)
  79. self.curl.perform()
  80. try:
  81. with nilmdb.utils.Iteratorizer(perform, curl_hack = True) as it:
  82. for i in it:
  83. if self._status == 200:
  84. # If we had a 200 response, yield the data to caller.
  85. yield i
  86. else:
  87. # Otherwise, collect it into an error string.
  88. error_body += i
  89. except pycurl.error as e:
  90. raise ServerError(status = "502 Error",
  91. url = self.url,
  92. message = e[1])
  93. # Raise an exception if there was an error
  94. self._check_error(error_body)
  95. def _req(self, url, params):
  96. """
  97. GET or POST that returns raw data. Returns the body
  98. data as a string, or raises an error if it contained an error.
  99. """
  100. self._setup_url(url, params)
  101. body = cStringIO.StringIO()
  102. self.curl.setopt(pycurl.WRITEFUNCTION, body.write)
  103. self._headers = ""
  104. def header_callback(data):
  105. self._headers += data
  106. self.curl.setopt(pycurl.HEADERFUNCTION, header_callback)
  107. try:
  108. self.curl.perform()
  109. except pycurl.error as e:
  110. raise ServerError(status = "502 Error",
  111. url = self.url,
  112. message = e[1])
  113. body_str = body.getvalue()
  114. # Raise an exception if there was an error
  115. self._check_error(body_str)
  116. return body_str
  117. def close(self):
  118. self.curl.close()
  119. def _iterate_lines(self, it):
  120. """
  121. Given an iterator that returns arbitrarily-sized chunks
  122. of data, return '\n'-delimited lines of text
  123. """
  124. partial = ""
  125. for chunk in it:
  126. partial += chunk
  127. lines = partial.split("\n")
  128. for line in lines[0:-1]:
  129. yield line
  130. partial = lines[-1]
  131. if partial != "":
  132. yield partial
  133. # Non-generator versions
  134. def _doreq(self, url, params, retjson):
  135. """
  136. Perform a request, and return the body.
  137. url: URL to request (relative to baseurl)
  138. params: dictionary of query parameters
  139. retjson: expect JSON and return python objects instead of string
  140. """
  141. out = self._req(url, params)
  142. if retjson:
  143. return json.loads(out)
  144. return out
  145. def get(self, url, params = None, retjson = True):
  146. """Simple GET"""
  147. self.curl.setopt(pycurl.UPLOAD, 0)
  148. return self._doreq(url, params, retjson)
  149. def put(self, url, postdata, params = None, retjson = True):
  150. """Simple PUT"""
  151. self.curl.setopt(pycurl.UPLOAD, 1)
  152. self._setup_url(url, params)
  153. data = cStringIO.StringIO(postdata)
  154. self.curl.setopt(pycurl.READFUNCTION, data.read)
  155. return self._doreq(url, params, retjson)
  156. # Generator versions
  157. def _doreq_gen(self, url, params, retjson):
  158. """
  159. Perform a request, and return lines of the body in a generator.
  160. url: URL to request (relative to baseurl)
  161. params: dictionary of query parameters
  162. retjson: expect JSON and yield python objects instead of strings
  163. """
  164. for line in self._iterate_lines(self._req_generator(url, params)):
  165. if retjson:
  166. yield json.loads(line)
  167. else:
  168. yield line
  169. def get_gen(self, url, params = None, retjson = True):
  170. """Simple GET, returning a generator"""
  171. self.curl.setopt(pycurl.UPLOAD, 0)
  172. return self._doreq_gen(url, params, retjson)
  173. def put_gen(self, url, postdata, params = None, retjson = True):
  174. """Simple PUT, returning a generator"""
  175. self.curl.setopt(pycurl.UPLOAD, 1)
  176. self._setup_url(url, params)
  177. data = cStringIO.StringIO(postdata)
  178. self.curl.setopt(pycurl.READFUNCTION, data.read)
  179. return self._doreq_gen(url, params, retjson)