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.
 
 
 

218 lines
7.5 KiB

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