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.
 
 
 

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