|
- """Client utilities for accessing NILM database via HTTP"""
-
- from __future__ import absolute_import
- from nilmdb.printf import *
-
- import time
- import sys
- import re
- import os
- import json
- import urlparse
- import urllib
- import pycurl
- import cStringIO
-
- version = "1.0"
-
- class Error(Exception):
- """Base exception for both ClientError and ServerError responses"""
- def __init__(self,
- status = "Unspecified error",
- message = None,
- url = None,
- traceback = None):
- Exception.__init__(self, status)
- self.status = status # e.g. "400 Bad Request"
- self.message = message # textual message from the server
- self.url = url # URL we were requesting
- self.traceback = traceback # server traceback, if available
- def __str__(self):
- s = sprintf("[%s]", self.status)
- if self.message:
- s += sprintf(" %s", self.message)
- if self.url:
- s += sprintf(" (%s)", self.url)
- if self.traceback: # pragma: no cover
- s += sprintf("\nServer traceback:\n%s", self.traceback)
- return s
- class ClientError(Error):
- pass
- class ServerError(Error):
- pass
-
- class MyCurl(object):
- """Curl wrapper for HTTP client requests"""
- def __init__(self, baseurl = ""):
- """If baseurl is supplied, all other functions that take
- a URL can be given a relative URL instead."""
- # Verify / clean up URL
- reparsed = urlparse.urlparse(baseurl).geturl()
- if '://' not in reparsed:
- reparsed = urlparse.urlparse("http://" + baseurl).geturl()
- self.baseurl = reparsed
- self.curl = pycurl.Curl()
- self.curl.setopt(pycurl.SSL_VERIFYHOST, 2)
- self.curl.setopt(pycurl.FOLLOWLOCATION, 1)
- self.curl.setopt(pycurl.MAXREDIRS, 5)
- self._setup_url()
-
- def _setup_url(self, url = "", params = ""):
- url = urlparse.urljoin(self.baseurl, url)
- if params:
- url = urlparse.urljoin(url, "?" + urllib.urlencode(params, True))
- self.curl.setopt(pycurl.URL, url)
- self.url = url
-
- def _check_error(self, body = None):
- code = self.curl.getinfo(pycurl.RESPONSE_CODE)
- if code == 200:
- return
- # Default variables for exception
- args = { "url" : self.url,
- "status" : str(code),
- "message" : None,
- "traceback" : None }
- try:
- # Fill with server-provided data if we can
- jsonerror = json.loads(body)
- args["status"] = jsonerror["status"]
- args["message"] = jsonerror["message"]
- args["traceback"] = jsonerror["traceback"]
- except Exception: # pragma: no cover
- pass
- if code >= 400 and code <= 499:
- raise ClientError(**args)
- else: # pragma: no cover
- if code >= 500 and code <= 599:
- raise ServerError(**args)
- else:
- raise Error(**args)
-
- def _reqjson(self, url, params):
- """GET or POST that returns JSON string"""
- self._setup_url(url, params)
- body = cStringIO.StringIO()
- self.curl.setopt(pycurl.WRITEFUNCTION, body.write)
- try:
- self.curl.perform()
- except pycurl.error as e:
- raise ServerError(status = "502 Error",
- url = self.url,
- message = e[1])
- body_str = body.getvalue()
- self._check_error(body_str)
- return json.loads(body_str)
-
- def close(self):
- self.curl.close()
-
- def getjson(self, url, params = None):
- """Simple GET that returns JSON string"""
- self.curl.setopt(pycurl.UPLOAD, 0)
- return self._reqjson(url, params)
-
- def putjson(self, url, postdata, params = None):
- """Simple PUT that returns JSON string"""
- self._setup_url(url, params)
- data = cStringIO.StringIO(postdata)
- self.curl.setopt(pycurl.UPLOAD, 1)
- self.curl.setopt(pycurl.READFUNCTION, data.read)
- return self._reqjson(url, params)
-
- class Client(object):
- """Main client interface to the Nilm database."""
-
- client_version = version
-
- def __init__(self, url):
- self.curl = MyCurl(url)
-
- def _json_param(self, data):
- """Return compact json-encoded version of parameter"""
- return json.dumps(data, separators=(',',':'))
-
- def close(self):
- self.curl.close()
-
- def geturl(self):
- """Return the URL we're using"""
- return self.curl.baseurl
-
- def version(self):
- """Return server version"""
- return self.curl.getjson("version")
-
- def dbpath(self):
- """Return server database path"""
- return self.curl.getjson("dbpath")
-
- def stream_list(self, path = None, layout = None):
- params = {}
- if path is not None:
- params["path"] = path
- if layout is not None:
- params["layout"] = layout
- return self.curl.getjson("stream/list", params)
-
- def stream_get_metadata(self, path, keys = None):
- params = { "path": path }
- if keys is not None:
- params["key"] = keys
- return self.curl.getjson("stream/get_metadata", params)
-
- def stream_set_metadata(self, path, data):
- """Set stream metadata from a dictionary, replacing all existing
- metadata."""
- params = {
- "path": path,
- "data": self._json_param(data)
- }
- return self.curl.getjson("stream/set_metadata", params)
-
- def stream_update_metadata(self, path, data):
- """Update stream metadata from a dictionary"""
- params = {
- "path": path,
- "data": self._json_param(data)
- }
- return self.curl.getjson("stream/update_metadata", params)
-
- def stream_create(self, path, layout, index = None):
- """Create a new stream"""
- params = { "path": path,
- "layout" : layout }
- if index is not None:
- params["index"] = index
- return self.curl.getjson("stream/create", params)
-
- def stream_insert(self, path, data):
- """Insert data into a stream. data should be a file-like object
- that provides ASCII data that matches the database layout for path."""
- params = { "path": path }
-
- # See design.md for a discussion of how much data to send.
- # These are soft limits -- actual data might be rounded up.
- max_data = 1048576
- max_time = 30
-
- def sendit():
- return self.curl.putjson("stream/insert", send_data, params)
-
- result = None
- start = time.time()
- send_data = ""
- for line in data:
- elapsed = time.time() - start
- send_data += line
-
- if (len(send_data) > max_data) or (elapsed > max_time):
- result = sendit()
- send_data = ""
- start = time.time()
- if len(send_data):
- result = sendit()
-
- # Return the most recent JSON result we got back, or None if
- # we didn't make any requests.
- return result
|