Compare commits

...

11 Commits

Author SHA1 Message Date
405c110fd7 Doc updates 2013-07-29 15:36:43 -04:00
274adcd856 Documentation updates 2013-07-27 19:51:09 -04:00
a1850c9c2c Misc documentation 2013-07-25 16:08:35 -04:00
6cd28b67b1 Support iterator protocol in Serializer 2013-07-24 14:52:26 -04:00
d6d215d53d Improve boolean HTTP parameter handling 2013-07-15 14:38:28 -04:00
e02143ddb2 Remove duplicated test 2013-07-14 15:30:53 -04:00
e275384d03 Fix WSGI docs again 2013-07-11 16:36:32 -04:00
a6a67ec15c Update WSGI docs 2013-07-10 14:16:25 -04:00
fc43107307 Fill out test coverage 2013-07-09 19:06:26 -04:00
90633413bb Add nilmdb.utils.interval.human_string function 2013-07-09 19:01:53 -04:00
c7c3aff0fb Add nilmdb.utils.interval.optimize function 2013-07-09 17:50:21 -04:00
11 changed files with 136 additions and 21 deletions

View File

@@ -19,12 +19,12 @@ Then, set up Apache with a configuration like:
<VirtualHost> <VirtualHost>
WSGIScriptAlias /nilmdb /home/nilm/nilmdb.wsgi WSGIScriptAlias /nilmdb /home/nilm/nilmdb.wsgi
WSGIApplicationGroup nilmdb-appgroup
WSGIProcessGroup nilmdb-procgroup
WSGIDaemonProcess nilmdb-procgroup threads=32 user=nilm group=nilm WSGIDaemonProcess nilmdb-procgroup threads=32 user=nilm group=nilm
# Access control example:
<Location /nilmdb> <Location /nilmdb>
WSGIProcessGroup nilmdb-procgroup
WSGIApplicationGroup nilmdb-appgroup
# Access control example:
Order deny,allow Order deny,allow
Deny from all Deny from all
Allow from 1.2.3.4 Allow from 1.2.3.4

View File

@@ -58,6 +58,11 @@ class Client(object):
return self.http.get("dbinfo") return self.http.get("dbinfo")
def stream_list(self, path = None, layout = None, extended = False): def stream_list(self, path = None, layout = None, extended = False):
"""Return a sorted list of [path, layout] lists. If 'path' or
'layout' are specified, only return streams that match those
exact values. If 'extended' is True, the returned lists have
extended info, e.g.: [path, layout, extent_min, extent_max,
total_rows, total_seconds."""
params = {} params = {}
if path is not None: if path is not None:
params["path"] = path params["path"] = path
@@ -69,6 +74,7 @@ class Client(object):
return nilmdb.utils.sort.sort_human(streams, key = lambda s: s[0]) return nilmdb.utils.sort.sort_human(streams, key = lambda s: s[0])
def stream_get_metadata(self, path, keys = None): def stream_get_metadata(self, path, keys = None):
"""Get stream metadata"""
params = { "path": path } params = { "path": path }
if keys is not None: if keys is not None:
params["key"] = keys params["key"] = keys

View File

@@ -27,6 +27,7 @@ from nilmdb.server.serverutil import (
json_error_page, json_error_page,
cherrypy_start, cherrypy_start,
cherrypy_stop, cherrypy_stop,
bool_param,
) )
# Add CORS_allow tool # Add CORS_allow tool
@@ -221,6 +222,8 @@ class Stream(NilmApp):
little-endian and matches the database types (including an little-endian and matches the database types (including an
int64 timestamp). int64 timestamp).
""" """
binary = bool_param(binary)
# Important that we always read the input before throwing any # Important that we always read the input before throwing any
# errors, to keep lengths happy for persistent connections. # errors, to keep lengths happy for persistent connections.
# Note that CherryPy 3.2.2 has a bug where this fails for GET # Note that CherryPy 3.2.2 has a bug where this fails for GET
@@ -345,6 +348,10 @@ class Stream(NilmApp):
little-endian and matches the database types (including an little-endian and matches the database types (including an
int64 timestamp). int64 timestamp).
""" """
binary = bool_param(binary)
markup = bool_param(markup)
count = bool_param(count)
(start, end) = self._get_times(start, end) (start, end) = self._get_times(start, end)
# Check path and get layout # Check path and get layout

View File

@@ -7,6 +7,21 @@ import os
import decorator import decorator
import simplejson as json import simplejson as json
# Helper to parse parameters into booleans
def bool_param(s):
"""Return a bool indicating whether parameter 's' was True or False,
supporting a few different types for 's'."""
try:
ss = s.lower()
if ss in [ "0", "false", "f", "no", "n" ]:
return False
if ss in [ "1", "true", "t", "yes", "y" ]:
return True
except Exception:
return bool(s)
raise cherrypy.HTTPError("400 Bad Request",
"can't parse parameter: " + ss)
# Decorators # Decorators
def chunked_response(func): def chunked_response(func):
"""Decorator to enable chunked responses.""" """Decorator to enable chunked responses."""

View File

@@ -1,5 +1,6 @@
"""Interval. Like nilmdb.server.interval, but re-implemented here """Interval. Like nilmdb.server.interval, but re-implemented here
in plain Python so clients have easier access to it. in plain Python so clients have easier access to it, and with a few
helper functions.
Intervals are half-open, ie. they include data points with timestamps Intervals are half-open, ie. they include data points with timestamps
[start, end) [start, end)
@@ -34,6 +35,10 @@ class Interval:
return ("[" + nilmdb.utils.time.timestamp_to_string(self.start) + return ("[" + nilmdb.utils.time.timestamp_to_string(self.start) +
" -> " + nilmdb.utils.time.timestamp_to_string(self.end) + ")") " -> " + nilmdb.utils.time.timestamp_to_string(self.end) + ")")
def human_string(self):
return ("[ " + nilmdb.utils.time.timestamp_to_human(self.start) +
" -> " + nilmdb.utils.time.timestamp_to_human(self.end) + " ]")
def __cmp__(self, other): def __cmp__(self, other):
"""Compare two intervals. If non-equal, order by start then end""" """Compare two intervals. If non-equal, order by start then end"""
return cmp(self.start, other.start) or cmp(self.end, other.end) return cmp(self.start, other.start) or cmp(self.end, other.end)
@@ -104,3 +109,20 @@ def set_difference(a, b):
b_interval = None b_interval = None
if a_interval: if a_interval:
out_start = ts out_start = ts
def optimize(it):
"""
Given an iterable 'it' with intervals, optimize them by joining
together intervals that are adjacent in time, and return a generator
that yields the new intervals.
"""
saved_int = None
for interval in it:
if saved_int is not None:
if saved_int.end == interval.start:
interval.start = saved_int.start
else:
yield saved_int
saved_int = interval
if saved_int is not None:
yield saved_int

View File

@@ -91,6 +91,20 @@ def serializer_proxy(obj_or_type):
r = SerializerCallProxy(self.__call_queue, attr, self) r = SerializerCallProxy(self.__call_queue, attr, self)
return r return r
# For an interable object, on __iter__(), save the object's
# iterator and return this proxy. On next(), call the object's
# iterator through this proxy.
def __iter__(self):
attr = getattr(self.__object, "__iter__")
self.__iter = SerializerCallProxy(self.__call_queue, attr, self)()
return self
def next(self):
return SerializerCallProxy(self.__call_queue,
self.__iter.next, self)()
def __getitem__(self, key):
return self.__getattr__("__getitem__")(key)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Call this to instantiate the type, if a type was passed """Call this to instantiate the type, if a type was passed
to serializer_proxy. Otherwise, pass the call through.""" to serializer_proxy. Otherwise, pass the call through."""

View File

@@ -60,7 +60,7 @@ def rate_to_period(hz, cycles = 1):
def parse_time(toparse): def parse_time(toparse):
""" """
Parse a free-form time string and return a nilmdb timestamp Parse a free-form time string and return a nilmdb timestamp
(integer seconds since epoch). If the string doesn't contain a (integer microseconds since epoch). If the string doesn't contain a
timestamp, the current local timezone is assumed (e.g. from the TZ timestamp, the current local timezone is assumed (e.g. from the TZ
env var). env var).
""" """

View File

@@ -1,7 +1,14 @@
import sys
if sys.version_info[0] >= 3: # pragma: no cover (future Python3 compat)
text_type = str
else:
text_type = unicode
def encode(u): def encode(u):
"""Try to encode something from Unicode to a string using the """Try to encode something from Unicode to a string using the
default encoding. If it fails, try encoding as UTF-8.""" default encoding. If it fails, try encoding as UTF-8."""
if not isinstance(u, unicode): if not isinstance(u, text_type):
return u return u
try: try:
return u.encode() return u.encode()
@@ -11,7 +18,7 @@ def encode(u):
def decode(s): def decode(s):
"""Try to decode someting from string to Unicode using the """Try to decode someting from string to Unicode using the
default encoding. If it fails, try decoding as UTF-8.""" default encoding. If it fails, try decoding as UTF-8."""
if isinstance(s, unicode): if isinstance(s, text_type):
return s return s
try: try:
return s.decode() return s.decode()

View File

@@ -354,10 +354,6 @@ class TestClient(object):
with assert_raises(ServerError) as e: with assert_raises(ServerError) as e:
client.http.get_gen("http://nosuchurl.example.com./").next() client.http.get_gen("http://nosuchurl.example.com./").next()
# Trigger a curl error in generator
with assert_raises(ServerError) as e:
client.http.get_gen("http://nosuchurl.example.com./").next()
# Check 404 for missing streams # Check 404 for missing streams
for function in [ client.stream_intervals, client.stream_extract ]: for function in [ client.stream_intervals, client.stream_extract ]:
with assert_raises(ClientError) as e: with assert_raises(ClientError) as e:
@@ -396,27 +392,38 @@ class TestClient(object):
headers()) headers())
# Extract # Extract
x = http.get("stream/extract", x = http.get("stream/extract", { "path": "/newton/prep",
{ "path": "/newton/prep", "start": "123", "end": "124" })
"start": "123",
"end": "124" })
if "transfer-encoding: chunked" not in headers(): if "transfer-encoding: chunked" not in headers():
warnings.warn("Non-chunked HTTP response for /stream/extract") warnings.warn("Non-chunked HTTP response for /stream/extract")
if "content-type: text/plain;charset=utf-8" not in headers(): if "content-type: text/plain;charset=utf-8" not in headers():
raise AssertionError("/stream/extract is not text/plain:\n" + raise AssertionError("/stream/extract is not text/plain:\n" +
headers()) headers())
x = http.get("stream/extract", x = http.get("stream/extract", { "path": "/newton/prep",
{ "path": "/newton/prep", "start": "123", "end": "124",
"start": "123", "binary": "1" })
"end": "124",
"binary": "1" })
if "transfer-encoding: chunked" not in headers(): if "transfer-encoding: chunked" not in headers():
warnings.warn("Non-chunked HTTP response for /stream/extract") warnings.warn("Non-chunked HTTP response for /stream/extract")
if "content-type: application/octet-stream" not in headers(): if "content-type: application/octet-stream" not in headers():
raise AssertionError("/stream/extract is not binary:\n" + raise AssertionError("/stream/extract is not binary:\n" +
headers()) headers())
# Make sure a binary of "0" is really off
x = http.get("stream/extract", { "path": "/newton/prep",
"start": "123", "end": "124",
"binary": "0" })
if "content-type: application/octet-stream" in headers():
raise AssertionError("/stream/extract is not text:\n" +
headers())
# Invalid parameters
with assert_raises(ClientError) as e:
x = http.get("stream/extract", { "path": "/newton/prep",
"start": "123", "end": "124",
"binary": "asdfasfd" })
in_("can't parse parameter", str(e.exception))
client.close() client.close()
def test_client_08_unicode(self): def test_client_08_unicode(self):

View File

@@ -59,6 +59,14 @@ class TestInterval:
self.test_interval_intersect() self.test_interval_intersect()
Interval = NilmdbInterval Interval = NilmdbInterval
# Other helpers in nilmdb.utils.interval
i = [ UtilsInterval(1,2), UtilsInterval(2,3), UtilsInterval(4,5) ]
eq_(list(nilmdb.utils.interval.optimize(i)),
[ UtilsInterval(1,3), UtilsInterval(4,5) ])
eq_(UtilsInterval(1234567890123456, 1234567890654321).human_string(),
"[ Fri, 13 Feb 2009 18:31:30.123456 -0500 -> " +
"Fri, 13 Feb 2009 18:31:30.654321 -0500 ]")
def test_interval(self): def test_interval(self):
# Test Interval class # Test Interval class
os.environ['TZ'] = "America/New_York" os.environ['TZ'] = "America/New_York"

View File

@@ -62,6 +62,28 @@ class Base(object):
eq_(self.foo.val, 20) eq_(self.foo.val, 20)
eq_(self.foo.init_thread, self.foo.test_thread) eq_(self.foo.init_thread, self.foo.test_thread)
class ListLike(object):
def __init__(self):
self.thread = threading.current_thread().name
self.foo = 0
def __iter__(self):
eq_(threading.current_thread().name, self.thread)
self.foo = 0
return self
def __getitem__(self, key):
eq_(threading.current_thread().name, self.thread)
return key
def next(self):
eq_(threading.current_thread().name, self.thread)
if self.foo < 5:
self.foo += 1
return self.foo
else:
raise StopIteration
class TestUnserialized(Base): class TestUnserialized(Base):
def setUp(self): def setUp(self):
self.foo = Foo() self.foo = Foo()
@@ -84,3 +106,10 @@ class TestSerializer(Base):
sp(sp(Foo("x"))).t() sp(sp(Foo("x"))).t()
sp(sp(Foo)("x")).t() sp(sp(Foo)("x")).t()
sp(sp(Foo))("x").t() sp(sp(Foo))("x").t()
def test_iter(self):
sp = nilmdb.utils.serializer_proxy
i = sp(ListLike)()
print iter(i)
eq_(list(i), [1,2,3,4,5])
eq_(i[3], 3)