|
- # -*- coding: utf-8 -*-
-
- """Class for performing HTTP client requests via libcurl"""
-
- import nilmdb
- import nilmdb.utils
- import nilmdb.client.httpclient
- from nilmdb.utils.printf import *
-
- import time
- import sys
- import re
- import os
- import simplejson as json
- import itertools
-
- version = "1.0"
-
- def float_to_string(f):
- # Use repr to maintain full precision in the string output.
- return repr(float(f))
-
- class Client(object):
- """Main client interface to the Nilm database."""
-
- client_version = version
-
- def __init__(self, url):
- self.http = nilmdb.client.httpclient.HTTPClient(url)
-
- def _json_param(self, data):
- """Return compact json-encoded version of parameter"""
- return json.dumps(data, separators=(',',':'))
-
- def close(self):
- self.http.close()
-
- def geturl(self):
- """Return the URL we're using"""
- return self.http.baseurl
-
- def version(self):
- """Return server version"""
- return self.http.get("version")
-
- def dbpath(self):
- """Return server database path"""
- return self.http.get("dbpath")
-
- def dbsize(self):
- """Return server database size as human readable string"""
- return self.http.get("dbsize")
-
- 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.http.get("stream/list", params)
-
- def stream_get_metadata(self, path, keys = None):
- params = { "path": path }
- if keys is not None:
- params["key"] = keys
- return self.http.get("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.http.get("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.http.get("stream/update_metadata", params)
-
- def stream_create(self, path, layout):
- """Create a new stream"""
- params = { "path": path,
- "layout" : layout }
- return self.http.get("stream/create", params)
-
- def stream_destroy(self, path):
- """Delete stream and its contents"""
- params = { "path": path }
- return self.http.get("stream/destroy", params)
-
- def stream_remove(self, path, start = None, end = None):
- """Remove data from the specified time range"""
- params = {
- "path": path
- }
- if start is not None:
- params["start"] = float_to_string(start)
- if end is not None:
- params["end"] = float_to_string(end)
- return self.http.get("stream/remove", params)
-
- def stream_insert(self, path, data, start = None, end = None):
- """Insert data into a stream. data should be a file-like object
- that provides ASCII data that matches the database layout for path.
-
- start and end are the starting and ending timestamp of this
- stream; all timestamps t in the data must satisfy 'start <= t
- < end'. If left unspecified, 'start' is the timestamp of the
- first line of data, and 'end' is the timestamp on the last line
- of data, plus a small delta of 1μs.
- """
- 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
- end_epsilon = 1e-6
-
-
- def extract_timestamp(line):
- return float(line.split()[0])
-
- def sendit():
- # If we have more data after this, use the timestamp of
- # the next line as the end. Otherwise, use the given
- # overall end time, or add end_epsilon to the last data
- # point.
- if nextline:
- block_end = extract_timestamp(nextline)
- if end and block_end > end:
- # This is unexpected, but we'll defer to the server
- # to return an error in this case.
- block_end = end
- elif end:
- block_end = end
- else:
- block_end = extract_timestamp(line) + end_epsilon
-
- # Send it
- params["start"] = float_to_string(block_start)
- params["end"] = float_to_string(block_end)
- return self.http.put("stream/insert", block_data, params)
-
- clock_start = time.time()
- block_data = ""
- block_start = start
- result = None
- for (line, nextline) in nilmdb.utils.misc.pairwise(data):
- # If we don't have a starting time, extract it from the first line
- if block_start is None:
- block_start = extract_timestamp(line)
-
- clock_elapsed = time.time() - clock_start
- block_data += line
-
- # If we have enough data, or enough time has elapsed,
- # send this block to the server, and empty things out
- # for the next block.
- if (len(block_data) > max_data) or (clock_elapsed > max_time):
- result = sendit()
- block_start = None
- block_data = ""
- clock_start = time.time()
-
- # One last block?
- if len(block_data):
- result = sendit()
-
- # Return the most recent JSON result we got back, or None if
- # we didn't make any requests.
- return result
-
- def stream_intervals(self, path, start = None, end = None):
- """
- Return a generator that yields each stream interval.
- """
- params = {
- "path": path
- }
- if start is not None:
- params["start"] = float_to_string(start)
- if end is not None:
- params["end"] = float_to_string(end)
- return self.http.get_gen("stream/intervals", params, retjson = True)
-
- def stream_extract(self, path, start = None, end = None, count = False):
- """
- Extract data from a stream. Returns a generator that yields
- lines of ASCII-formatted data that matches the database
- layout for the given path.
-
- Specify count=True to just get a count of values rather than
- the actual data.
- """
- params = {
- "path": path,
- }
- if start is not None:
- params["start"] = float_to_string(start)
- if end is not None:
- params["end"] = float_to_string(end)
- if count:
- params["count"] = 1
-
- return self.http.get_gen("stream/extract", params, retjson = False)
|