''' Created on 21.05.2011 @author: kca ''' from base64 import b64encode from cStringIO import StringIO from logging import DEBUG from socket import getservbyname from urllib2 import quote from urlparse import urlparse #import vertx from aplus import Promise from futile import ObjectProxy from futile.logging import LoggerMixin from futile.net.http.exc import NetworkError, HTTPError def compose_qs(values): return "&".join([ "%s=%s" % (quote(k), quote(v)) for k, v in dict(values).iteritems() ]) class LoggingResponseWrapper(LoggerMixin, ObjectProxy): def __init__(self, response, *args, **kw): super(LoggingResponseWrapper, self).__init__(proxyobject = response, *args, **kw) self.__buffer = StringIO() self.__finalized = False def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __enter__(self): return self def read(self, n = None): s = self._o.read(n) self.__buffer.write(s) return s def readline(self): s = self._o.readline() self.__buffer.write(s) return s def readlines(self, sizehint = None): lines = self._o.readlines(sizehint) self.__buffer.write(''.join(lines)) return lines def close(self): if self.__finalized: self.logger.debug("%s is already finalized" % (self, )) return self.__finalized = True try: if not self._o.isclosed(): self.__buffer.write(self._o.read()) self.logger.debug("Read data:\n %s", self.__buffer.getvalue()) except: self.logger.exception("Finalizing response failed") finally: self._o.close() self.__buffer.close() class CachingHttplibResponseWrapper(ObjectProxy, LoggerMixin): def __init__(self, response, path, tag, last_modified, cache, *args, **kw): super(CachingHttplibResponseWrapper, self).__init__(proxyobject = response, *args, **kw) self.__cache = cache self.__buffer = StringIO() self.__path = path self.__tag = tag self.__last_modified = last_modified self.__finalized = False def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __enter__(self): return self def read(self, n = None): s = self._o.read(n) self.__buffer.write(s) return s def readline(self): s = self._o.readline() self.__buffer.write(s) return s def readlines(self, sizehint = None): lines = self._o.readlines(sizehint) self.__buffer.write(''.join(lines)) return lines def close(self): if self.__finalized: self.logger.debug("%s is already finalized" % (self, )) return self.__finalized = True try: if not self._o.isclosed(): self.__buffer.write(self._o.read()) val = self.__buffer.getvalue() self.logger.debug("Putting to cache: %s -> %s, %s\n %s", self.__path, self.__tag, self.__last_modified, val) self.__cache[self.__path] = (self.__tag, self.__last_modified, val) except: self.logger.exception("Finalizing response failed") finally: self._o.close() self.__buffer.close() def __getattr__(self, name): return getattr(self._o, name) class closing(ObjectProxy): def __getattr__(self, k): return getattr(self._o, k) def __enter__(self): return self._o def __exit__(self, exc_type, exc_val, exc_tb): self._o.close() def close(self): self._o.close() class RestClient(LoggerMixin): ERROR_RESPONSE_MAX = 320 get_timeout = timeout = 120.0 def __init__(self, uri, username=None, password=None, certfile=None, keyfile=None, content_type="text/plain", headers=None, cache=True, timeout=None, get_timeout=None, component_name = "server", connection_manager = None, *args, **kw): super(RestClient, self).__init__(*args, **kw) self.logger.debug("Creating RestClient for %s", uri) self.timeout = timeout or self.timeout self.get_timeout = get_timeout or timeout or self.get_timeout if cache: if cache is True: from futile.caching import LRUCache cache = LRUCache() self.__cache = cache if "://" not in uri: uri = "http://" + uri self.__content_type = content_type self.component_name = component_name info = urlparse(uri) if info.scheme == "https": if bool(certfile) ^ bool(keyfile): raise ValueError("Must give both certfile and keyfile if any") if certfile: from os.path import exists if not exists(certfile): raise ValueError("Certificate file not found: %s" % (certfile, )) if not exists(keyfile): raise ValueError("Key file not found: %s" % (keyfile, )) elif info.scheme != "http": raise ValueError(info.scheme) port = info.port and int(info.port) or getservbyname(info.scheme) self.__base = info.path or "" #if not self.__base.endswith("/"): # self.__base += "/" if not username: username = info.username if not headers: headers = {} headers.setdefault("Accept", "*/*") headers["Accept-Encoding"] = "identity" if username: password = password or info.password or "" headers["Authorization"] = "Basic " + b64encode("%s:%s" % (username, password)) self.__headers = headers #if not connection_manager: # #from SimpleConnectionManager import SimpleConnectionManager as connection_manager # from ConnectionPoolManager import ConnectionPoolManager as connection_manager # # self.__connection_manager = connection_manager(host = info.hostname, port = port, # certfile = certfile, keyfile = keyfile, force_ssl = info.scheme == "https") # self.client= vertx.create_http_client() self.client.host = info.netloc.split(":")[0] self.client.port = port #temporary test server #import json #self.srv = vertx.create_http_server() #def srv_handle(re): # re.response.put_header("Content-Type","application/json; charset=utf-8") # re.response.put_header("Location","locationlocation.location") # re.response.end(json.dumps({"One":"Two"})) #self.srv.request_handler(srv_handle) #self.srv.listen(5000) def request(self, method, path, data = None, headers = {}, args = None): if isinstance(data, unicode): data = data.encode("utf-8") fullpath = self.__base + path request_headers = self.__headers.copy() if args: fullpath += ("?" in fullpath and "&" or "?") + compose_qs(args) if headers: request_headers.update(headers) if method == "GET": timeout = self.get_timeout try: etag, modified, cached = self.__cache[fullpath] if etag: request_headers["If-None-Match"] = etag request_headers["If-Modified-Since"] = modified except KeyError: request_headers.pop("If-None-Match", None) request_headers.pop("If-Modified-Since", None) else: timeout = self.timeout request_headers.setdefault("Content-Type", self.__content_type) log_headers = request_headers if self.logger.isEnabledFor(DEBUG) and "Authorization" in request_headers: log_headers = request_headers.copy() log_headers["Authorization"] = "" if method == "GET": self.logger.debug("%s: %s (%s)", method, fullpath, log_headers) else: self.logger.debug("%s: %s (%s)\n%s", method, fullpath, log_headers, repr(data)) #t = time() promise=Promise() try: #response = self.__connection_manager.request(method, fullpath, data, request_headers, timeout) def response_handler(resp): if resp.status_code == 304: try: promise.fulfill(closing(StringIO(cached))) except NameError: promise.reject(NetworkError("Error: The %s returned 304 though no cached version is available. Request was: %s %s" % (self.component_name, method, fullpath))) if resp.status_code < 200 or resp.status_code >= 300: try: promise.reject(HTTPError(msg = resp.status_message, status = resp.status_code)) except: promise.reject(HTTPError(msg = "Http error", status = response.status)) else: promise.fulfill(resp) req=self.client.request(method,fullpath,response_handler) for head,value in request_headers.items(): req.put_header(head,value) if data: req.chunked = True req.write_str(data) req.end() except Exception as e: print "Exception triggered: %s"%e promise.reject(e) return promise #if method == "DELETE": # try: # self.__cache.pop(fullpath, None) # except AttributeError: # pass #else: # etag = response.getheader("Etag") # modified = response.getheader("Last-Modified") # if etag or modified: # if not modified: # modified = datetime.utcnow().strftime("%a, %d %b %Y %X GMT") # response = CachingHttplibResponseWrapper(response, fullpath, etag, modified, self.__cache) # elif self.logger.isEnabledFor(DEBUG): # response = LoggingResponseWrapper(response) def get(self, path, headers = None, args = None): p = self.request("GET", path, headers = headers, args = args) return p def post(self, path, data, headers = None): p = self.request("POST", path, data, headers) return p add = post def put(self, path, data, headers = None): p = self.request("PUT", path, data) return p update = put def delete(self, path, headers = None): p = self.request("DELETE", path, None, headers) return p