Compare commits

...

51 Commits

Author SHA1 Message Date
c083d63c96 Tests for Unicode compliance 2013-01-03 17:03:52 -05:00
0221e3ea21 Update commandline test helpers to better handle Unicode
We replace cStringIO with StringIO subclass that forces UTF-8
encoding, and explicitly convert commandlines to UTF-8 before
shlex.  These changes will only affect tests, not normal commandline
operation.
2013-01-03 17:03:52 -05:00
f5fd2b064e Replace urllib.encode() with a version that encodes Unicode as UTF-8 instead 2013-01-03 17:02:38 -05:00
06e91a6a98 Always use function version of print() 2013-01-03 17:02:38 -05:00
41b3f3c018 Always use UTF-8 for filenames in nilmdb.bulkdata 2013-01-03 17:02:38 -05:00
842076fef4 Cleanup server error handling with decorator 2013-01-03 17:02:38 -05:00
10d58f6a47 More test coverage 2013-01-02 00:00:05 -05:00
e2464efc12 Test everything; remove debugging 2013-01-01 23:46:54 -05:00
1beae5024e Bulkdata extract works now. 2013-01-01 23:44:52 -05:00
c7c65b6542 Work around CherryPy bug #1200; related cleanups
Spent way too long trying to track down a cryptic error that turned
out to be a CherryPy bug.  Now we catch this using a decorator in the
'extract' and 'intervals' generators that transforms exceptions that
trigger the bugs into one that does not.  fun!
2013-01-01 23:03:53 -05:00
f41ff0a6e8 Inserting bulk data is essentially done, not tested 2013-01-01 21:04:35 -05:00
389c1d189f Make option to turn off chunked encoding for debugging more clear. 2013-01-01 21:03:33 -05:00
487298986e More work towards bulkdata 2012-12-31 18:44:57 -05:00
d4cd045c48 Fix path stuff, build packer in bulkdata.Table 2012-12-31 17:22:30 -05:00
3816645313 More work on BulkData 2012-12-31 17:22:30 -05:00
83b937c720 More Pytables -> bulkdata conversion 2012-12-31 17:22:30 -05:00
b3e6e8976f More work towards flat bulk data storage.
Cleaned up OS-specific path handling in nilmdb, bulkdata.
2012-12-31 17:22:30 -05:00
c890ea93cb WIP switching away from PyTables 2012-12-31 17:22:29 -05:00
84c68c6913 Better documentation, cache Tables 2012-12-31 17:22:29 -05:00
6f1e6fe232 Isolate all PyTables stuff to a single file.
This will make migrating to my own data storage engine easier.
2012-12-31 17:22:29 -05:00
b0d76312d1 Add must_close() decorator, use it in nilmdb
Warns at runtime if a class's close() method wasn't called before the
object was destroyed.
2012-12-31 17:21:19 -05:00
19c846c71c Remove outdated files 2012-12-31 15:55:43 -05:00
f355c73209 Refactor utility classes into nilmdb.utils subdir/namespace
There's some bug with the testing harness where placing e.g.
  from du import du
in nilmdb/utils/__init__.py doesn't quite work -- sometimes the
module "du" replaces the function "du".  Not exactly sure why;
we work around that by just renaming files so they don't match
the imported names directly.
2012-12-31 15:55:36 -05:00
173014ba19 Use nilmdb.lrucache for caching interval sets 2012-12-31 14:52:46 -05:00
24d4752bc3 Add LRU cache memoizing decorator for functions 2012-12-31 14:39:16 -05:00
a85b273e2e Remove compression.
Messes up extraction, since we random access for the timestamp binary
search.  In the future, maybe switching to multiple tables (one for
timestamp, one for compressed data) would be smart.
2012-12-14 17:19:23 -05:00
7f73b4b304 Use compression in pytables 2012-12-14 17:17:52 -05:00
f3eb6d1b79 Time it! 2012-12-14 16:57:02 -05:00
9082cc9f44 Merging adjacent intervals is working now!
Adjust test expectations accordingly, since the number of intervals
they print out will now be smaller.
2012-12-12 19:25:27 -05:00
bf64a40472 Some misc test additions, interval optimizations. Still need adjacency test 2012-12-11 23:31:55 -05:00
32dbeebc09 More insertion checks. Need to get interval concatenation working. 2012-12-11 18:08:00 -05:00
66ddc79b15 Inserting works again, with proper end/start for paired blocks.
timeit.sh script works too!
2012-12-07 20:30:39 -05:00
7a8bd0bf41 Don't include layout on client side 2012-12-07 16:24:15 -05:00
ee552de740 Start reworking/fixing insert timestamps 2012-12-06 20:25:24 -05:00
6d1fb61573 Use 'repr' instead of 'str' in Interval string representation.
Otherwise timestamps get truncated to 2 decimal places.
2012-12-05 17:47:48 -05:00
f094529e66 TODO update 2012-12-04 22:15:53 -05:00
5fecec2a4c Support deleting streams with new 'destroy' command 2012-12-04 22:15:00 -05:00
85bb46f45c Use pytable's createparents flag to avoid having to create group
structure manually.
2012-12-04 18:57:36 -05:00
17c329fd6d Start to be a little more strict about how intervals are half-open. 2012-11-29 15:35:11 -05:00
437e1b425a More speed tests, some whitespace cleanups 2012-11-29 15:22:47 -05:00
c0f87db3c1 Converted rbtree, interval to Cython. Serious speedups! 2012-11-29 15:13:09 -05:00
a9c5c19e30 Start converting interval.py to Cython. 2012-11-29 12:42:38 -05:00
f39567b2bc Speed updates 2012-11-29 01:35:01 -05:00
99ec0f4946 Converted rbtree.py to Cython
About 3x faster
2012-11-29 01:25:51 -05:00
f5c60f68dc Speed tests.
test_interval_speed is about O(n * log n), which is good -- but the
constants are high and it hits swap on a 4G machine for the 2**21
test.  Hopefully cython helps!
2012-11-29 01:00:54 -05:00
bdef0986d6 rbtree and interval tests fully pass now.
On to benchmarking...
2012-11-29 00:42:50 -05:00
c396c4dac8 rbtree tests complete 2012-11-29 00:07:49 -05:00
0b443f510b Filling out rbtree tests, search routines 2012-11-28 20:57:23 -05:00
66fa6f3824 Add rendering test 2012-11-28 18:34:51 -05:00
875fbe969f Some documentation and other cleanups in rbtree.py 2012-11-28 18:30:21 -05:00
e35e85886e add .gitignore 2012-11-28 17:21:51 -05:00
53 changed files with 1750 additions and 1687 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
db/
tests/*testdb/
.coverage
*.pyc

View File

@@ -1,2 +1,4 @@
sudo apt-get install python-nose python-coverage sudo apt-get install python-nose python-coverage
sudo apt-get install python-tables cython python-cherrypy3 sudo apt-get install python-tables python-cherrypy3
sudo apt-get install cython # 0.17.1-1 or newer

4
TODO
View File

@@ -1,5 +1 @@
- Merge adjacent intervals on insert (maybe with client help?) - Merge adjacent intervals on insert (maybe with client help?)
- Better testing:
- see about getting coverage on layout.pyx
- layout.pyx performance tests, before and after generalization

View File

@@ -103,13 +103,13 @@ Speed
- First approach was quadratic. Adding four hours of data: - First approach was quadratic. Adding four hours of data:
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/1/raw $ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/1/raw
real 24m31.093s real 24m31.093s
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/1/raw $ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/1/raw
real 43m44.528s real 43m44.528s
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-130002 /bpnilm/1/raw $ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-130002 /bpnilm/1/raw
real 93m29.713s real 93m29.713s
$ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-140003 /bpnilm/1/raw $ time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-140003 /bpnilm/1/raw
real 166m53.007s real 166m53.007s
- Disabling pytables indexing didn't help: - Disabling pytables indexing didn't help:
@@ -122,19 +122,19 @@ Speed
- Server RAM usage is constant. - Server RAM usage is constant.
- Speed problems were due to IntervalSet speed, of parsing intervals - Speed problems were due to IntervalSet speed, of parsing intervals
from the database and adding the new one each time. from the database and adding the new one each time.
- First optimization is to cache result of `nilmdb:_get_intervals`, - First optimization is to cache result of `nilmdb:_get_intervals`,
which gives the best speedup. which gives the best speedup.
- Also switched to internally using bxInterval from bx-python package. - Also switched to internally using bxInterval from bx-python package.
Speed of `tests/test_interval:TestIntervalSpeed` is pretty decent Speed of `tests/test_interval:TestIntervalSpeed` is pretty decent
and seems to be growing logarithmically now. About 85μs per insertion and seems to be growing logarithmically now. About 85μs per insertion
for inserting 131k entries. for inserting 131k entries.
- Storing the interval data in SQL might be better, with a scheme like: - Storing the interval data in SQL might be better, with a scheme like:
http://www.logarithmic.net/pfh/blog/01235197474 http://www.logarithmic.net/pfh/blog/01235197474
- Next slowdown target is nilmdb.layout.Parser.parse(). - Next slowdown target is nilmdb.layout.Parser.parse().
- Rewrote parsers using cython and sscanf - Rewrote parsers using cython and sscanf
- Stats (rev 10831), with _add_interval disabled - Stats (rev 10831), with _add_interval disabled
@@ -142,7 +142,14 @@ Speed
layout.pyx.parse:63 13913 sec, 5.1g calls layout.pyx.parse:63 13913 sec, 5.1g calls
numpy:records.py.fromrecords:569 7410 sec, 262k calls numpy:records.py.fromrecords:569 7410 sec, 262k calls
- Probably OK for now. - Probably OK for now.
- After all updates, now takes about 8.5 minutes to insert an hour of
data, constant after adding 171 hours (4.9 billion data points)
- Data set size: 98 gigs = 20 bytes per data point.
6 uint16 data + 1 uint32 timestamp = 16 bytes per point
So compression must be off -- will retry with compression forced on.
IntervalSet speed IntervalSet speed
----------------- -----------------
- Initial implementation was pretty slow, even with binary search in - Initial implementation was pretty slow, even with binary search in
@@ -161,6 +168,18 @@ IntervalSet speed
- Might be algorithmic improvements to be made in Interval.py, - Might be algorithmic improvements to be made in Interval.py,
like in `__and__` like in `__and__`
- Replaced again with rbtree. Seems decent. Numbers are time per
insert for 2**17 insertions, followed by total wall time and RAM
usage for running "make test" with `test_rbtree` and `test_interval`
with range(5,20):
- old values with bxinterval:
20.2 μS, total 20 s, 177 MB RAM
- rbtree, plain python:
97 μS, total 105 s, 846 MB RAM
- rbtree converted to cython:
26 μS, total 29 s, 320 MB RAM
- rbtree and interval converted to cython:
8.4 μS, total 12 s, 134 MB RAM
Layouts Layouts
------- -------
@@ -170,12 +189,12 @@ just collections and counts of a single type. We'll still use strings
to describe them, with format: to describe them, with format:
type_count type_count
where type is "uint16", "float32", or "float64", and count is an integer. where type is "uint16", "float32", or "float64", and count is an integer.
nilmdb.layout.named() will parse these strings into the appropriate nilmdb.layout.named() will parse these strings into the appropriate
handlers. For compatibility: handlers. For compatibility:
"RawData" == "uint16_6" "RawData" == "uint16_6"
"RawNotchedData" == "uint16_9" "RawNotchedData" == "uint16_9"
"PrepData" == "float32_8" "PrepData" == "float32_8"

View File

@@ -1,605 +0,0 @@
// The RedBlackEntry class is an Abstract Base Class. This means that no
// instance of the RedBlackEntry class can exist. Only classes which
// inherit from the RedBlackEntry class can exist. Furthermore any class
// which inherits from the RedBlackEntry class must define the member
// function GetKey(). The Print() member function does not have to
// be defined because a default definition exists.
//
// The GetKey() function should return an integer key for that entry.
// The key for an entry should never change otherwise bad things might occur.
class RedBlackEntry {
public:
RedBlackEntry();
virtual ~RedBlackEntry();
virtual int GetKey() const = 0;
virtual void Print() const;
};
class RedBlackTreeNode {
friend class RedBlackTree;
public:
void Print(RedBlackTreeNode*,
RedBlackTreeNode*) const;
RedBlackTreeNode();
RedBlackTreeNode(RedBlackEntry *);
RedBlackEntry * GetEntry() const;
~RedBlackTreeNode();
protected:
RedBlackEntry * storedEntry;
int key;
int red; /* if red=0 then the node is black */
RedBlackTreeNode * left;
RedBlackTreeNode * right;
RedBlackTreeNode * parent;
};
class RedBlackTree {
public:
RedBlackTree();
~RedBlackTree();
void Print() const;
RedBlackEntry * DeleteNode(RedBlackTreeNode *);
RedBlackTreeNode * Insert(RedBlackEntry *);
RedBlackTreeNode * GetPredecessorOf(RedBlackTreeNode *) const;
RedBlackTreeNode * GetSuccessorOf(RedBlackTreeNode *) const;
RedBlackTreeNode * Search(int key);
TemplateStack<RedBlackTreeNode *> * Enumerate(int low, int high) ;
void CheckAssumptions() const;
protected:
/* A sentinel is used for root and for nil. These sentinels are */
/* created when RedBlackTreeCreate is caled. root->left should always */
/* point to the node which is the root of the tree. nil points to a */
/* node which should always be black but has aribtrary children and */
/* parent and no key or info. The point of using these sentinels is so */
/* that the root and nil nodes do not require special cases in the code */
RedBlackTreeNode * root;
RedBlackTreeNode * nil;
void LeftRotate(RedBlackTreeNode *);
void RightRotate(RedBlackTreeNode *);
void TreeInsertHelp(RedBlackTreeNode *);
void TreePrintHelper(RedBlackTreeNode *) const;
void FixUpMaxHigh(RedBlackTreeNode *);
void DeleteFixUp(RedBlackTreeNode *);
};
const int MIN_INT=-MAX_INT;
RedBlackTreeNode::RedBlackTreeNode(){
};
RedBlackTreeNode::RedBlackTreeNode(RedBlackEntry * newEntry)
: storedEntry (newEntry) , key(newEntry->GetKey()) {
};
RedBlackTreeNode::~RedBlackTreeNode(){
};
RedBlackEntry * RedBlackTreeNode::GetEntry() const {return storedEntry;}
RedBlackEntry::RedBlackEntry(){
};
RedBlackEntry::~RedBlackEntry(){
};
void RedBlackEntry::Print() const {
cout << "No Print Method defined. Using Default: " << GetKey() << endl;
}
RedBlackTree::RedBlackTree()
{
nil = new RedBlackTreeNode;
nil->left = nil->right = nil->parent = nil;
nil->red = 0;
nil->key = MIN_INT;
nil->storedEntry = NULL;
root = new RedBlackTreeNode;
root->parent = root->left = root->right = nil;
root->key = MAX_INT;
root->red=0;
root->storedEntry = NULL;
}
/***********************************************************************/
/* FUNCTION: LeftRotate */
/**/
/* INPUTS: the node to rotate on */
/**/
/* OUTPUT: None */
/**/
/* Modifies Input: this, x */
/**/
/* EFFECTS: Rotates as described in _Introduction_To_Algorithms by */
/* Cormen, Leiserson, Rivest (Chapter 14). Basically this */
/* makes the parent of x be to the left of x, x the parent of */
/* its parent before the rotation and fixes other pointers */
/* accordingly. */
/***********************************************************************/
void RedBlackTree::LeftRotate(RedBlackTreeNode* x) {
RedBlackTreeNode* y;
/* I originally wrote this function to use the sentinel for */
/* nil to avoid checking for nil. However this introduces a */
/* very subtle bug because sometimes this function modifies */
/* the parent pointer of nil. This can be a problem if a */
/* function which calls LeftRotate also uses the nil sentinel */
/* and expects the nil sentinel's parent pointer to be unchanged */
/* after calling this function. For example, when DeleteFixUP */
/* calls LeftRotate it expects the parent pointer of nil to be */
/* unchanged. */
y=x->right;
x->right=y->left;
if (y->left != nil) y->left->parent=x; /* used to use sentinel here */
/* and do an unconditional assignment instead of testing for nil */
y->parent=x->parent;
/* instead of checking if x->parent is the root as in the book, we */
/* count on the root sentinel to implicitly take care of this case */
if( x == x->parent->left) {
x->parent->left=y;
} else {
x->parent->right=y;
}
y->left=x;
x->parent=y;
}
/***********************************************************************/
/* FUNCTION: RighttRotate */
/**/
/* INPUTS: node to rotate on */
/**/
/* OUTPUT: None */
/**/
/* Modifies Input?: this, y */
/**/
/* EFFECTS: Rotates as described in _Introduction_To_Algorithms by */
/* Cormen, Leiserson, Rivest (Chapter 14). Basically this */
/* makes the parent of x be to the left of x, x the parent of */
/* its parent before the rotation and fixes other pointers */
/* accordingly. */
/***********************************************************************/
void RedBlackTree::RightRotate(RedBlackTreeNode* y) {
RedBlackTreeNode* x;
/* I originally wrote this function to use the sentinel for */
/* nil to avoid checking for nil. However this introduces a */
/* very subtle bug because sometimes this function modifies */
/* the parent pointer of nil. This can be a problem if a */
/* function which calls LeftRotate also uses the nil sentinel */
/* and expects the nil sentinel's parent pointer to be unchanged */
/* after calling this function. For example, when DeleteFixUP */
/* calls LeftRotate it expects the parent pointer of nil to be */
/* unchanged. */
x=y->left;
y->left=x->right;
if (nil != x->right) x->right->parent=y; /*used to use sentinel here */
/* and do an unconditional assignment instead of testing for nil */
/* instead of checking if x->parent is the root as in the book, we */
/* count on the root sentinel to implicitly take care of this case */
x->parent=y->parent;
if( y == y->parent->left) {
y->parent->left=x;
} else {
y->parent->right=x;
}
x->right=y;
y->parent=x;
}
/***********************************************************************/
/* FUNCTION: TreeInsertHelp */
/**/
/* INPUTS: z is the node to insert */
/**/
/* OUTPUT: none */
/**/
/* Modifies Input: this, z */
/**/
/* EFFECTS: Inserts z into the tree as if it were a regular binary tree */
/* using the algorithm described in _Introduction_To_Algorithms_ */
/* by Cormen et al. This funciton is only intended to be called */
/* by the Insert function and not by the user */
/***********************************************************************/
void RedBlackTree::TreeInsertHelp(RedBlackTreeNode* z) {
/* This function should only be called by RedBlackTree::Insert */
RedBlackTreeNode* x;
RedBlackTreeNode* y;
z->left=z->right=nil;
y=root;
x=root->left;
while( x != nil) {
y=x;
if ( x->key > z->key) {
x=x->left;
} else { /* x->key <= z->key */
x=x->right;
}
}
z->parent=y;
if ( (y == root) ||
(y->key > z->key) ) {
y->left=z;
} else {
y->right=z;
}
}
/* Before calling InsertNode the node x should have its key set */
/***********************************************************************/
/* FUNCTION: InsertNode */
/**/
/* INPUTS: newEntry is the entry to insert*/
/**/
/* OUTPUT: This function returns a pointer to the newly inserted node */
/* which is guarunteed to be valid until this node is deleted. */
/* What this means is if another data structure stores this */
/* pointer then the tree does not need to be searched when this */
/* is to be deleted. */
/**/
/* Modifies Input: tree */
/**/
/* EFFECTS: Creates a node node which contains the appropriate key and */
/* info pointers and inserts it into the tree. */
/***********************************************************************/
/* jim */
RedBlackTreeNode * RedBlackTree::Insert(RedBlackEntry * newEntry)
{
RedBlackTreeNode * y;
RedBlackTreeNode * x;
RedBlackTreeNode * newNode;
x = new RedBlackTreeNode(newEntry);
TreeInsertHelp(x);
newNode = x;
x->red=1;
while(x->parent->red) { /* use sentinel instead of checking for root */
if (x->parent == x->parent->parent->left) {
y=x->parent->parent->right;
if (y->red) {
x->parent->red=0;
y->red=0;
x->parent->parent->red=1;
x=x->parent->parent;
} else {
if (x == x->parent->right) {
x=x->parent;
LeftRotate(x);
}
x->parent->red=0;
x->parent->parent->red=1;
RightRotate(x->parent->parent);
}
} else { /* case for x->parent == x->parent->parent->right */
/* this part is just like the section above with */
/* left and right interchanged */
y=x->parent->parent->left;
if (y->red) {
x->parent->red=0;
y->red=0;
x->parent->parent->red=1;
x=x->parent->parent;
} else {
if (x == x->parent->left) {
x=x->parent;
RightRotate(x);
}
x->parent->red=0;
x->parent->parent->red=1;
LeftRotate(x->parent->parent);
}
}
}
root->left->red=0;
return(newNode);
}
/***********************************************************************/
/* FUNCTION: GetSuccessorOf */
/**/
/* INPUTS: x is the node we want the succesor of */
/**/
/* OUTPUT: This function returns the successor of x or NULL if no */
/* successor exists. */
/**/
/* Modifies Input: none */
/**/
/* Note: uses the algorithm in _Introduction_To_Algorithms_ */
/***********************************************************************/
RedBlackTreeNode * RedBlackTree::GetSuccessorOf(RedBlackTreeNode * x) const
{
RedBlackTreeNode* y;
if (nil != (y = x->right)) { /* assignment to y is intentional */
while(y->left != nil) { /* returns the minium of the right subtree of x */
y=y->left;
}
return(y);
} else {
y=x->parent;
while(x == y->right) { /* sentinel used instead of checking for nil */
x=y;
y=y->parent;
}
if (y == root) return(nil);
return(y);
}
}
/***********************************************************************/
/* FUNCTION: GetPredecessorOf */
/**/
/* INPUTS: x is the node to get predecessor of */
/**/
/* OUTPUT: This function returns the predecessor of x or NULL if no */
/* predecessor exists. */
/**/
/* Modifies Input: none */
/**/
/* Note: uses the algorithm in _Introduction_To_Algorithms_ */
/***********************************************************************/
RedBlackTreeNode * RedBlackTree::GetPredecessorOf(RedBlackTreeNode * x) const {
RedBlackTreeNode* y;
if (nil != (y = x->left)) { /* assignment to y is intentional */
while(y->right != nil) { /* returns the maximum of the left subtree of x */
y=y->right;
}
return(y);
} else {
y=x->parent;
while(x == y->left) {
if (y == root) return(nil);
x=y;
y=y->parent;
}
return(y);
}
}
/***********************************************************************/
/* FUNCTION: Print */
/**/
/* INPUTS: none */
/**/
/* OUTPUT: none */
/**/
/* EFFECTS: This function recursively prints the nodes of the tree */
/* inorder. */
/**/
/* Modifies Input: none */
/**/
/* Note: This function should only be called from ITTreePrint */
/***********************************************************************/
void RedBlackTreeNode::Print(RedBlackTreeNode * nil,
RedBlackTreeNode * root) const {
storedEntry->Print();
printf(", key=%i ",key);
printf(" l->key=");
if( left == nil) printf("NULL"); else printf("%i",left->key);
printf(" r->key=");
if( right == nil) printf("NULL"); else printf("%i",right->key);
printf(" p->key=");
if( parent == root) printf("NULL"); else printf("%i",parent->key);
printf(" red=%i\n",red);
}
void RedBlackTree::TreePrintHelper( RedBlackTreeNode* x) const {
if (x != nil) {
TreePrintHelper(x->left);
x->Print(nil,root);
TreePrintHelper(x->right);
}
}
/***********************************************************************/
/* FUNCTION: Print */
/**/
/* INPUTS: none */
/**/
/* OUTPUT: none */
/**/
/* EFFECT: This function recursively prints the nodes of the tree */
/* inorder. */
/**/
/* Modifies Input: none */
/**/
/***********************************************************************/
void RedBlackTree::Print() const {
TreePrintHelper(root->left);
}
/***********************************************************************/
/* FUNCTION: DeleteFixUp */
/**/
/* INPUTS: x is the child of the spliced */
/* out node in DeleteNode. */
/**/
/* OUTPUT: none */
/**/
/* EFFECT: Performs rotations and changes colors to restore red-black */
/* properties after a node is deleted */
/**/
/* Modifies Input: this, x */
/**/
/* The algorithm from this function is from _Introduction_To_Algorithms_ */
/***********************************************************************/
void RedBlackTree::DeleteFixUp(RedBlackTreeNode* x) {
RedBlackTreeNode * w;
RedBlackTreeNode * rootLeft = root->left;
while( (!x->red) && (rootLeft != x)) {
if (x == x->parent->left) {
//
w=x->parent->right;
if (w->red) {
w->red=0;
x->parent->red=1;
LeftRotate(x->parent);
w=x->parent->right;
}
if ( (!w->right->red) && (!w->left->red) ) {
w->red=1;
x=x->parent;
} else {
if (!w->right->red) {
w->left->red=0;
w->red=1;
RightRotate(w);
w=x->parent->right;
}
w->red=x->parent->red;
x->parent->red=0;
w->right->red=0;
LeftRotate(x->parent);
x=rootLeft; /* this is to exit while loop */
}
//
} else { /* the code below is has left and right switched from above */
w=x->parent->left;
if (w->red) {
w->red=0;
x->parent->red=1;
RightRotate(x->parent);
w=x->parent->left;
}
if ( (!w->right->red) && (!w->left->red) ) {
w->red=1;
x=x->parent;
} else {
if (!w->left->red) {
w->right->red=0;
w->red=1;
LeftRotate(w);
w=x->parent->left;
}
w->red=x->parent->red;
x->parent->red=0;
w->left->red=0;
RightRotate(x->parent);
x=rootLeft; /* this is to exit while loop */
}
}
}
x->red=0;
}
/***********************************************************************/
/* FUNCTION: DeleteNode */
/**/
/* INPUTS: tree is the tree to delete node z from */
/**/
/* OUTPUT: returns the RedBlackEntry stored at deleted node */
/**/
/* EFFECT: Deletes z from tree and but don't call destructor */
/**/
/* Modifies Input: z */
/**/
/* The algorithm from this function is from _Introduction_To_Algorithms_ */
/***********************************************************************/
RedBlackEntry * RedBlackTree::DeleteNode(RedBlackTreeNode * z){
RedBlackTreeNode* y;
RedBlackTreeNode* x;
RedBlackEntry * returnValue = z->storedEntry;
y= ((z->left == nil) || (z->right == nil)) ? z : GetSuccessorOf(z);
x= (y->left == nil) ? y->right : y->left;
if (root == (x->parent = y->parent)) { /* assignment of y->p to x->p is intentional */
root->left=x;
} else {
if (y == y->parent->left) {
y->parent->left=x;
} else {
y->parent->right=x;
}
}
if (y != z) { /* y should not be nil in this case */
/* y is the node to splice out and x is its child */
y->left=z->left;
y->right=z->right;
y->parent=z->parent;
z->left->parent=z->right->parent=y;
if (z == z->parent->left) {
z->parent->left=y;
} else {
z->parent->right=y;
}
if (!(y->red)) {
y->red = z->red;
DeleteFixUp(x);
} else
y->red = z->red;
delete z;
} else {
if (!(y->red)) DeleteFixUp(x);
delete y;
}
return returnValue;
}
/***********************************************************************/
/* FUNCTION: Enumerate */
/**/
/* INPUTS: tree is the tree to look for keys between [low,high] */
/**/
/* OUTPUT: stack containing pointers to the nodes between [low,high] */
/**/
/* Modifies Input: none */
/**/
/* EFFECT: Returns a stack containing pointers to nodes containing */
/* keys which in [low,high]/ */
/**/
/***********************************************************************/
TemplateStack<RedBlackTreeNode *> * RedBlackTree::Enumerate(int low,
int high) {
TemplateStack<RedBlackTreeNode *> * enumResultStack =
new TemplateStack<RedBlackTreeNode *>(4);
RedBlackTreeNode* x=root->left;
RedBlackTreeNode* lastBest=NULL;
while(nil != x) {
if ( x->key > high ) {
x=x->left;
} else {
lastBest=x;
x=x->right;
}
}
while ( (lastBest) && (low <= lastBest->key) ) {
enumResultStack->Push(lastBest);
lastBest=GetPredecessorOf(lastBest);
}
return(enumResultStack);
}

View File

@@ -3,14 +3,10 @@
from .nilmdb import NilmDB from .nilmdb import NilmDB
from .server import Server from .server import Server
from .client import Client from .client import Client
from .timer import Timer
import cmdline
import pyximport; pyximport.install() import pyximport; pyximport.install()
import layout import layout
import serializer
import timestamper
import interval import interval
import du
import cmdline

310
nilmdb/bulkdata.py Normal file
View File

@@ -0,0 +1,310 @@
# Fixed record size bulk data storage
from __future__ import absolute_import
from __future__ import division
import nilmdb
from nilmdb.utils.printf import *
import os
import sys
import cPickle as pickle
import struct
import fnmatch
import mmap
# Up to 256 open file descriptors at any given time
table_cache_size = 16
fd_cache_size = 16
@nilmdb.utils.must_close()
class BulkData(object):
def __init__(self, basepath):
self.basepath = basepath
self.root = os.path.join(self.basepath, "data")
# Make root path
if not os.path.isdir(self.root):
os.mkdir(self.root)
def close(self):
self.getnode.cache_remove_all()
def _encode_filename(self, path):
# Encode all paths to UTF-8, regardless of sys.getfilesystemencoding(),
# because we want to be able to represent all code points and the user
# will never be directly exposed to filenames. We can then do path
# manipulations on the UTF-8 directly.
if isinstance(path, unicode):
return path.encode('utf-8')
return path
def create(self, unicodepath, layout_name):
"""
unicodepath: path to the data (e.g. u'/newton/prep').
Paths must contain at least two elements, e.g.:
/newton/prep
/newton/raw
/newton/upstairs/prep
/newton/upstairs/raw
layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8'
"""
path = self._encode_filename(unicodepath)
if path[0] != '/':
raise ValueError("paths must start with /")
[ group, node ] = path.rsplit("/", 1)
if group == '':
raise ValueError("invalid path")
# Get layout, and build format string for struct module
try:
layout = nilmdb.layout.get_named(layout_name)
struct_fmt = '<d' # Little endian, double timestamp
struct_mapping = {
"int8": 'b',
"uint8": 'B',
"int16": 'h',
"uint16": 'H',
"int32": 'i',
"uint32": 'I',
"int64": 'q',
"uint64": 'Q',
"float32": 'f',
"float64": 'd',
}
for n in range(layout.count):
struct_fmt += struct_mapping[layout.datatype]
except KeyError:
raise ValueError("no such layout, or bad data types")
# Create the table. Note that we make a distinction here
# between NilmDB paths (always Unix style, split apart
# manually) and OS paths (built up with os.path.join)
try:
# Make directories leading up to this one
elements = path.lstrip('/').split('/')
for i in range(len(elements)):
ospath = os.path.join(self.root, *elements[0:i])
if Table.exists(ospath):
raise ValueError("path is subdir of existing node")
if not os.path.isdir(ospath):
os.mkdir(ospath)
# Make the final dir
ospath = os.path.join(self.root, *elements)
if os.path.isdir(ospath):
raise ValueError("subdirs of this path already exist")
os.mkdir(ospath)
# Write format string to file
Table.create(ospath, struct_fmt)
except OSError as e:
raise ValueError("error creating table at that path: " + e.strerror)
# Open and cache it
self.getnode(unicodepath)
# Success
return
def destroy(self, unicodepath):
"""Fully remove all data at a particular path. No way to undo
it! The group/path structure is removed, too."""
path = self._encode_filename(unicodepath)
# Get OS path
elements = path.lstrip('/').split('/')
ospath = os.path.join(self.root, *elements)
# Remove Table object from cache
self.getnode.cache_remove(self, ospath)
# Remove the contents of the target directory
if not os.path.isfile(os.path.join(ospath, "format")):
raise ValueError("nothing at that path")
for file in os.listdir(ospath):
os.remove(os.path.join(ospath, file))
# Remove empty parent directories
for i in reversed(range(len(elements))):
ospath = os.path.join(self.root, *elements[0:i+1])
try:
os.rmdir(ospath)
except OSError:
break
# Cache open tables
@nilmdb.utils.lru_cache(size = table_cache_size,
onremove = lambda x: x.close())
def getnode(self, unicodepath):
"""Return a Table object corresponding to the given database
path, which must exist."""
path = self._encode_filename(unicodepath)
elements = path.lstrip('/').split('/')
ospath = os.path.join(self.root, *elements)
return Table(ospath)
@nilmdb.utils.must_close()
class Table(object):
"""Tools to help access a single table (data at a specific OS path)"""
# Class methods, to help keep format details in this class.
@classmethod
def exists(cls, root):
"""Return True if a table appears to exist at this OS path"""
return os.path.isfile(os.path.join(root, "format"))
@classmethod
def create(cls, root, struct_fmt):
"""Initialize a table at the given OS path.
'struct_fmt' is a Struct module format description"""
format = { "rows_per_file": 4 * 1024 * 1024,
"struct_fmt": struct_fmt }
with open(os.path.join(root, "format"), "wb") as f:
pickle.dump(format, f, 2)
# Normal methods
def __init__(self, root):
"""'root' is the full OS path to the directory of this table"""
self.root = root
# Load the format and build packer
with open(self._fullpath("format"), "rb") as f:
format = pickle.load(f)
self.rows_per_file = format["rows_per_file"]
self.packer = struct.Struct(format["struct_fmt"])
self.file_size = self.packer.size * self.rows_per_file
# Find nrows by locating the lexicographically last filename
# and using its size.
pattern = '[0-9a-f]' * 8
allfiles = fnmatch.filter(os.listdir(self.root), pattern)
if allfiles:
filename = max(allfiles)
offset = os.path.getsize(self._fullpath(filename))
self.nrows = self._row_from_fnoffset(filename, offset)
else:
self.nrows = 0
def close(self):
self.mmap_open.cache_remove_all()
# Internal helpers
def _fullpath(self, filename):
return os.path.join(self.root, filename)
def _fnoffset_from_row(self, row):
"""Return a (filename, offset, count) tuple:
filename: the filename that contains the specified row
offset: byte offset of the specified row within the file
count: number of rows (starting at offste) that fit in the file
"""
filenum = row // self.rows_per_file
filename = sprintf("%08x", filenum)
offset = (row % self.rows_per_file) * self.packer.size
count = self.rows_per_file - (row % self.rows_per_file)
return (filename, offset, count)
def _row_from_fnoffset(self, filename, offset):
"""Return the row number that corresponds to the given
filename and byte-offset within that file."""
filenum = int(filename, 16)
if (offset % self.packer.size) != 0:
raise ValueError("file offset is not a multiple of data size")
row = (filenum * self.rows_per_file) + (offset // self.packer.size)
return row
# Cache open files
@nilmdb.utils.lru_cache(size = fd_cache_size,
onremove = lambda x: x.close())
def mmap_open(self, file, newsize = None):
"""Open and map a given filename (relative to self.root).
Will be automatically closed when evicted from the cache.
If 'newsize' is provided, the file is truncated to the given
size before the mapping is returned. (Note that the LRU cache
on this function means the truncate will only happen if the
object isn't already cached; mmap.resize should be used too)"""
f = open(os.path.join(self.root, file), "a+", 0)
if newsize is not None:
# mmap can't map a zero-length file, so this allows the
# caller to set the filesize between file creation and
# mmap.
f.truncate(newsize)
mm = mmap.mmap(f.fileno(), 0)
return mm
def append(self, data):
"""Append the data and flush it to disk.
data is a nested Python list [[row],[row],[...]]"""
remaining = len(data)
dataiter = iter(data)
while remaining:
# See how many rows we can fit into the current file, and open it
(filename, offset, count) = self._fnoffset_from_row(self.nrows)
if count > remaining:
count = remaining
newsize = offset + count * self.packer.size
mm = self.mmap_open(filename, newsize)
mm.seek(offset)
# Extend the file to the target length. We specified
# newsize when opening, but that may have been ignored if
# the mmap_open returned a cached object.
mm.resize(newsize)
# Write the data
for i in xrange(count):
row = dataiter.next()
mm.write(self.packer.pack(*row))
remaining -= count
self.nrows += count
def __getitem__(self, key):
"""Extract data and return it. Supports simple indexing
(table[n]) and range slices (table[n:m]). Returns a nested
Python list [[row],[row],[...]]"""
# Handle simple slices
if isinstance(key, slice):
# Fall back to brute force if the slice isn't simple
if ((key.step is not None and key.step != 1) or
key.start is None or
key.stop is None or
key.start >= key.stop or
key.start < 0 or
key.stop > self.nrows):
return [ self[x] for x in xrange(*key.indices(self.nrows)) ]
ret = []
row = key.start
remaining = key.stop - key.start
while remaining:
(filename, offset, count) = self._fnoffset_from_row(row)
if count > remaining:
count = remaining
mm = self.mmap_open(filename)
for i in xrange(count):
ret.append(list(self.packer.unpack_from(mm, offset)))
offset += self.packer.size
remaining -= count
row += count
return ret
# Handle single points
if key < 0 or key >= self.nrows:
raise IndexError("Index out of range")
(filename, offset, count) = self._fnoffset_from_row(key)
mm = self.mmap_open(filename)
# unpack_from ignores the mmap object's current seek position
return self.packer.unpack_from(mm, offset)
class TimestampOnlyTable(object):
"""Helper that lets us pass a Tables object into bisect, by
returning only the timestamp when a particular row is requested."""
def __init__(self, table):
self.table = table
def __getitem__(self, index):
return self.table[index][0]

View File

@@ -1,495 +0,0 @@
# cython: profile=False
# This is from bx-python 554:07aca5a9f6fc (BSD licensed), modified to
# store interval ranges as doubles rather than 32-bit integers.
"""
Data structure for performing intersect queries on a set of intervals which
preserves all information about the intervals (unlike bitset projection methods).
:Authors: James Taylor (james@jamestaylor.org),
Ian Schenk (ian.schenck@gmail.com),
Brent Pedersen (bpederse@gmail.com)
"""
# Historical note:
# This module original contained an implementation based on sorted endpoints
# and a binary search, using an idea from Scott Schwartz and Piotr Berman.
# Later an interval tree implementation was implemented by Ian for Galaxy's
# join tool (see `bx.intervals.operations.quicksect.py`). This was then
# converted to Cython by Brent, who also added support for
# upstream/downstream/neighbor queries. This was modified by James to
# handle half-open intervals strictly, to maintain sort order, and to
# implement the same interface as the original Intersecter.
#cython: cdivision=True
import operator
cdef extern from "stdlib.h":
int ceil(float f)
float log(float f)
int RAND_MAX
int rand()
int strlen(char *)
int iabs(int)
cdef inline double dmax2(double a, double b):
if b > a: return b
return a
cdef inline double dmax3(double a, double b, double c):
if b > a:
if c > b:
return c
return b
if a > c:
return a
return c
cdef inline double dmin3(double a, double b, double c):
if b < a:
if c < b:
return c
return b
if a < c:
return a
return c
cdef inline double dmin2(double a, double b):
if b < a: return b
return a
cdef float nlog = -1.0 / log(0.5)
cdef class IntervalNode:
"""
A single node of an `IntervalTree`.
NOTE: Unless you really know what you are doing, you probably should us
`IntervalTree` rather than using this directly.
"""
cdef float priority
cdef public object interval
cdef public double start, end
cdef double minend, maxend, minstart
cdef IntervalNode cleft, cright, croot
property left_node:
def __get__(self):
return self.cleft if self.cleft is not EmptyNode else None
property right_node:
def __get__(self):
return self.cright if self.cright is not EmptyNode else None
property root_node:
def __get__(self):
return self.croot if self.croot is not EmptyNode else None
def __repr__(self):
return "IntervalNode(%g, %g)" % (self.start, self.end)
def __cinit__(IntervalNode self, double start, double end, object interval):
# Python lacks the binomial distribution, so we convert a
# uniform into a binomial because it naturally scales with
# tree size. Also, python's uniform is perfect since the
# upper limit is not inclusive, which gives us undefined here.
self.priority = ceil(nlog * log(-1.0/(1.0 * rand()/RAND_MAX - 1)))
self.start = start
self.end = end
self.interval = interval
self.maxend = end
self.minstart = start
self.minend = end
self.cleft = EmptyNode
self.cright = EmptyNode
self.croot = EmptyNode
cpdef IntervalNode insert(IntervalNode self, double start, double end, object interval):
"""
Insert a new IntervalNode into the tree of which this node is
currently the root. The return value is the new root of the tree (which
may or may not be this node!)
"""
cdef IntervalNode croot = self
# If starts are the same, decide which to add interval to based on
# end, thus maintaining sortedness relative to start/end
cdef double decision_endpoint = start
if start == self.start:
decision_endpoint = end
if decision_endpoint > self.start:
# insert to cright tree
if self.cright is not EmptyNode:
self.cright = self.cright.insert( start, end, interval )
else:
self.cright = IntervalNode( start, end, interval )
# rebalance tree
if self.priority < self.cright.priority:
croot = self.rotate_left()
else:
# insert to cleft tree
if self.cleft is not EmptyNode:
self.cleft = self.cleft.insert( start, end, interval)
else:
self.cleft = IntervalNode( start, end, interval)
# rebalance tree
if self.priority < self.cleft.priority:
croot = self.rotate_right()
croot.set_ends()
self.cleft.croot = croot
self.cright.croot = croot
return croot
cdef IntervalNode rotate_right(IntervalNode self):
cdef IntervalNode croot = self.cleft
self.cleft = self.cleft.cright
croot.cright = self
self.set_ends()
return croot
cdef IntervalNode rotate_left(IntervalNode self):
cdef IntervalNode croot = self.cright
self.cright = self.cright.cleft
croot.cleft = self
self.set_ends()
return croot
cdef inline void set_ends(IntervalNode self):
if self.cright is not EmptyNode and self.cleft is not EmptyNode:
self.maxend = dmax3(self.end, self.cright.maxend, self.cleft.maxend)
self.minend = dmin3(self.end, self.cright.minend, self.cleft.minend)
self.minstart = dmin3(self.start, self.cright.minstart, self.cleft.minstart)
elif self.cright is not EmptyNode:
self.maxend = dmax2(self.end, self.cright.maxend)
self.minend = dmin2(self.end, self.cright.minend)
self.minstart = dmin2(self.start, self.cright.minstart)
elif self.cleft is not EmptyNode:
self.maxend = dmax2(self.end, self.cleft.maxend)
self.minend = dmin2(self.end, self.cleft.minend)
self.minstart = dmin2(self.start, self.cleft.minstart)
def intersect( self, double start, double end, sort=True ):
"""
given a start and a end, return a list of features
falling within that range
"""
cdef list results = []
self._intersect( start, end, results )
if sort:
results = sorted(results)
return results
find = intersect
cdef void _intersect( IntervalNode self, double start, double end, list results):
# Left subtree
if self.cleft is not EmptyNode and self.cleft.maxend > start:
self.cleft._intersect( start, end, results )
# This interval
if ( self.end > start ) and ( self.start < end ):
results.append( self.interval )
# Right subtree
if self.cright is not EmptyNode and self.start < end:
self.cright._intersect( start, end, results )
cdef void _seek_left(IntervalNode self, double position, list results, int n, double max_dist):
# we know we can bail in these 2 cases.
if self.maxend + max_dist < position:
return
if self.minstart > position:
return
# the ordering of these 3 blocks makes it so the results are
# ordered nearest to farest from the query position
if self.cright is not EmptyNode:
self.cright._seek_left(position, results, n, max_dist)
if -1 < position - self.end < max_dist:
results.append(self.interval)
# TODO: can these conditionals be more stringent?
if self.cleft is not EmptyNode:
self.cleft._seek_left(position, results, n, max_dist)
cdef void _seek_right(IntervalNode self, double position, list results, int n, double max_dist):
# we know we can bail in these 2 cases.
if self.maxend < position: return
if self.minstart - max_dist > position: return
#print "SEEK_RIGHT:",self, self.cleft, self.maxend, self.minstart, position
# the ordering of these 3 blocks makes it so the results are
# ordered nearest to farest from the query position
if self.cleft is not EmptyNode:
self.cleft._seek_right(position, results, n, max_dist)
if -1 < self.start - position < max_dist:
results.append(self.interval)
if self.cright is not EmptyNode:
self.cright._seek_right(position, results, n, max_dist)
cpdef left(self, position, int n=1, double max_dist=2500):
"""
find n features with a start > than `position`
f: a Interval object (or anything with an `end` attribute)
n: the number of features to return
max_dist: the maximum distance to look before giving up.
"""
cdef list results = []
# use start - 1 becuase .left() assumes strictly left-of
self._seek_left( position - 1, results, n, max_dist )
if len(results) == n: return results
r = results
r.sort(key=operator.attrgetter('end'), reverse=True)
return r[:n]
cpdef right(self, position, int n=1, double max_dist=2500):
"""
find n features with a end < than position
f: a Interval object (or anything with a `start` attribute)
n: the number of features to return
max_dist: the maximum distance to look before giving up.
"""
cdef list results = []
# use end + 1 becuase .right() assumes strictly right-of
self._seek_right(position + 1, results, n, max_dist)
if len(results) == n: return results
r = results
r.sort(key=operator.attrgetter('start'))
return r[:n]
def traverse(self):
if self.cleft is not EmptyNode:
for node in self.cleft.traverse():
yield node
yield self.interval
if self.cright is not EmptyNode:
for node in self.cright.traverse():
yield node
cdef IntervalNode EmptyNode = IntervalNode( 0, 0, Interval(0, 0))
## ---- Wrappers that retain the old interface -------------------------------
cdef class Interval:
"""
Basic feature, with required integer start and end properties.
Also accepts optional strand as +1 or -1 (used for up/downstream queries),
a name, and any arbitrary data is sent in on the info keyword argument
>>> from bx.intervals.intersection import Interval
>>> f1 = Interval(23, 36)
>>> f2 = Interval(34, 48, value={'chr':12, 'anno':'transposon'})
>>> f2
Interval(34, 48, value={'anno': 'transposon', 'chr': 12})
"""
cdef public double start, end
cdef public object value, chrom, strand
def __init__(self, double start, double end, object value=None, object chrom=None, object strand=None ):
assert start <= end, "start must be less than end"
self.start = start
self.end = end
self.value = value
self.chrom = chrom
self.strand = strand
def __repr__(self):
fstr = "Interval(%g, %g" % (self.start, self.end)
if not self.value is None:
fstr += ", value=" + str(self.value)
fstr += ")"
return fstr
def __richcmp__(self, other, op):
if op == 0:
# <
return self.start < other.start or self.end < other.end
elif op == 1:
# <=
return self == other or self < other
elif op == 2:
# ==
return self.start == other.start and self.end == other.end
elif op == 3:
# !=
return self.start != other.start or self.end != other.end
elif op == 4:
# >
return self.start > other.start or self.end > other.end
elif op == 5:
# >=
return self == other or self > other
cdef class IntervalTree:
"""
Data structure for performing window intersect queries on a set of
of possibly overlapping 1d intervals.
Usage
=====
Create an empty IntervalTree
>>> from bx.intervals.intersection import Interval, IntervalTree
>>> intersecter = IntervalTree()
An interval is a start and end position and a value (possibly None).
You can add any object as an interval:
>>> intersecter.insert( 0, 10, "food" )
>>> intersecter.insert( 3, 7, dict(foo='bar') )
>>> intersecter.find( 2, 5 )
['food', {'foo': 'bar'}]
If the object has start and end attributes (like the Interval class) there
is are some shortcuts:
>>> intersecter = IntervalTree()
>>> intersecter.insert_interval( Interval( 0, 10 ) )
>>> intersecter.insert_interval( Interval( 3, 7 ) )
>>> intersecter.insert_interval( Interval( 3, 40 ) )
>>> intersecter.insert_interval( Interval( 13, 50 ) )
>>> intersecter.find( 30, 50 )
[Interval(3, 40), Interval(13, 50)]
>>> intersecter.find( 100, 200 )
[]
Before/after for intervals
>>> intersecter.before_interval( Interval( 10, 20 ) )
[Interval(3, 7)]
>>> intersecter.before_interval( Interval( 5, 20 ) )
[]
Upstream/downstream
>>> intersecter.upstream_of_interval(Interval(11, 12))
[Interval(0, 10)]
>>> intersecter.upstream_of_interval(Interval(11, 12, strand="-"))
[Interval(13, 50)]
>>> intersecter.upstream_of_interval(Interval(1, 2, strand="-"), num_intervals=3)
[Interval(3, 7), Interval(3, 40), Interval(13, 50)]
"""
cdef IntervalNode root
def __cinit__( self ):
root = None
# ---- Position based interfaces -----------------------------------------
def insert( self, double start, double end, object value=None ):
"""
Insert the interval [start,end) associated with value `value`.
"""
if self.root is None:
self.root = IntervalNode( start, end, value )
else:
self.root = self.root.insert( start, end, value )
add = insert
def find( self, start, end ):
"""
Return a sorted list of all intervals overlapping [start,end).
"""
if self.root is None:
return []
return self.root.find( start, end )
def before( self, position, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie before `position` and are no
further than `max_dist` positions away
"""
if self.root is None:
return []
return self.root.left( position, num_intervals, max_dist )
def after( self, position, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie after `position` and are no
further than `max_dist` positions away
"""
if self.root is None:
return []
return self.root.right( position, num_intervals, max_dist )
# ---- Interval-like object based interfaces -----------------------------
def insert_interval( self, interval ):
"""
Insert an "interval" like object (one with at least start and end
attributes)
"""
self.insert( interval.start, interval.end, interval )
add_interval = insert_interval
def before_interval( self, interval, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie completely before `interval`
and are no further than `max_dist` positions away
"""
if self.root is None:
return []
return self.root.left( interval.start, num_intervals, max_dist )
def after_interval( self, interval, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie completely after `interval` and
are no further than `max_dist` positions away
"""
if self.root is None:
return []
return self.root.right( interval.end, num_intervals, max_dist )
def upstream_of_interval( self, interval, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie completely upstream of
`interval` and are no further than `max_dist` positions away
"""
if self.root is None:
return []
if interval.strand == -1 or interval.strand == "-":
return self.root.right( interval.end, num_intervals, max_dist )
else:
return self.root.left( interval.start, num_intervals, max_dist )
def downstream_of_interval( self, interval, num_intervals=1, max_dist=2500 ):
"""
Find `num_intervals` intervals that lie completely downstream of
`interval` and are no further than `max_dist` positions away
"""
if self.root is None:
return []
if interval.strand == -1 or interval.strand == "-":
return self.root.left( interval.start, num_intervals, max_dist )
else:
return self.root.right( interval.end, num_intervals, max_dist )
def traverse(self):
"""
iterator that traverses the tree
"""
if self.root is None:
return iter([])
return self.root.traverse()
# For backward compatibility
Intersecter = IntervalTree

View File

@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
"""Class for performing HTTP client requests via libcurl""" """Class for performing HTTP client requests via libcurl"""
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import time import time
import sys import sys
import re import re
import os import os
import simplejson as json import simplejson as json
import itertools
import nilmdb.httpclient import nilmdb.httpclient
@@ -16,6 +19,10 @@ from nilmdb.httpclient import ClientError, ServerError, Error
version = "1.0" 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): class Client(object):
"""Main client interface to the Nilm database.""" """Main client interface to the Nilm database."""
@@ -84,33 +91,82 @@ class Client(object):
"layout" : layout } "layout" : layout }
return self.http.get("stream/create", params) return self.http.get("stream/create", params)
def stream_insert(self, path, data): def stream_destroy(self, path):
"""Delete stream and its contents"""
params = { "path": path }
return self.http.get("stream/destroy", params)
def stream_insert(self, path, data, start = None, end = None):
"""Insert data into a stream. data should be a file-like object """Insert data into a stream. data should be a file-like object
that provides ASCII data that matches the database layout for path.""" 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 } params = { "path": path }
# See design.md for a discussion of how much data to send. # See design.md for a discussion of how much data to send.
# These are soft limits -- actual data might be rounded up. # These are soft limits -- actual data might be rounded up.
max_data = 1048576 max_data = 1048576
max_time = 30 max_time = 30
end_epsilon = 1e-6
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), ..., (sn,None)"
a, b = itertools.tee(iterable)
next(b, None)
return itertools.izip_longest(a, b)
def extract_timestamp(line):
return float(line.split()[0])
def sendit(): def sendit():
result = self.http.put("stream/insert", send_data, params) # If we have more data after this, use the timestamp of
params["old_timestamp"] = result[1] # the next line as the end. Otherwise, use the given
return result # 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 result = None
start = time.time() for (line, nextline) in pairwise(data):
send_data = "" # If we don't have a starting time, extract it from the first line
for line in data: if block_start is None:
elapsed = time.time() - start block_start = extract_timestamp(line)
send_data += line
if (len(send_data) > max_data) or (elapsed > max_time): 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() result = sendit()
send_data = "" block_start = None
start = time.time() block_data = ""
if len(send_data): clock_start = time.time()
# One last block?
if len(block_data):
result = sendit() result = sendit()
# Return the most recent JSON result we got back, or None if # Return the most recent JSON result we got back, or None if
@@ -125,9 +181,9 @@ class Client(object):
"path": path "path": path
} }
if start is not None: if start is not None:
params["start"] = repr(start) # use repr to keep precision params["start"] = float_to_string(start)
if end is not None: if end is not None:
params["end"] = repr(end) params["end"] = float_to_string(end)
return self.http.get_gen("stream/intervals", params, retjson = True) return self.http.get_gen("stream/intervals", params, retjson = True)
def stream_extract(self, path, start = None, end = None, count = False): def stream_extract(self, path, start = None, end = None, count = False):
@@ -143,9 +199,9 @@ class Client(object):
"path": path, "path": path,
} }
if start is not None: if start is not None:
params["start"] = repr(start) # use repr to keep precision params["start"] = float_to_string(start)
if end is not None: if end is not None:
params["end"] = repr(end) params["end"] = float_to_string(end)
if count: if count:
params["count"] = 1 params["count"] = 1

View File

@@ -1,7 +1,7 @@
"""Command line client functionality""" """Command line client functionality"""
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
import datetime_tz import datetime_tz
@@ -15,7 +15,8 @@ version = "0.1"
# Valid subcommands. Defined in separate files just to break # Valid subcommands. Defined in separate files just to break
# things up -- they're still called with Cmdline as self. # things up -- they're still called with Cmdline as self.
subcommands = [ "info", "create", "list", "metadata", "insert", "extract" ] subcommands = [ "info", "create", "list", "metadata", "insert", "extract",
"destroy" ]
# Import the subcommand modules. Equivalent way of doing this would be # Import the subcommand modules. Equivalent way of doing this would be
# from . import info as cmd_info # from . import info as cmd_info

View File

@@ -1,5 +1,5 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
from argparse import ArgumentDefaultsHelpFormatter as def_form from argparse import ArgumentDefaultsHelpFormatter as def_form

25
nilmdb/cmdline/destroy.py Normal file
View File

@@ -0,0 +1,25 @@
from __future__ import absolute_import
from nilmdb.utils.printf import *
import nilmdb.client
from argparse import ArgumentDefaultsHelpFormatter as def_form
def setup(self, sub):
cmd = sub.add_parser("destroy", help="Delete a stream and all data",
formatter_class = def_form,
description="""
Destroy the stream at the specified path. All
data and metadata related to the stream is
permanently deleted.
""")
cmd.set_defaults(handler = cmd_destroy)
group = cmd.add_argument_group("Required arguments")
group.add_argument("path",
help="Path of the stream to delete, e.g. /foo/bar")
def cmd_destroy(self):
"""Destroy stream"""
try:
self.client.stream_destroy(self.args.path)
except nilmdb.client.ClientError as e:
self.die("Error deleting stream: %s", str(e))

View File

@@ -1,7 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from __future__ import print_function
from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
import nilmdb.layout
import sys import sys
def setup(self, sub): def setup(self, sub):
@@ -51,7 +51,7 @@ def cmd_extract(self):
# Strip timestamp (first element). Doesn't make sense # Strip timestamp (first element). Doesn't make sense
# if we are only returning a count. # if we are only returning a count.
dataline = ' '.join(dataline.split(' ')[1:]) dataline = ' '.join(dataline.split(' ')[1:])
print dataline print(dataline)
printed = True printed = True
if not printed: if not printed:
if self.args.annotate: if self.args.annotate:

View File

@@ -1,5 +1,5 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
from argparse import ArgumentDefaultsHelpFormatter as def_form from argparse import ArgumentDefaultsHelpFormatter as def_form

View File

@@ -1,7 +1,6 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
import nilmdb.layout
import nilmdb.timestamper import nilmdb.timestamper
import sys import sys

View File

@@ -1,5 +1,5 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
import fnmatch import fnmatch

View File

@@ -1,5 +1,5 @@
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.client import nilmdb.client
def setup(self, sub): def setup(self, sub):

View File

@@ -1,7 +1,8 @@
"""HTTP client library""" """HTTP client library"""
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.utils
import time import time
import sys import sys
@@ -9,12 +10,9 @@ import re
import os import os
import simplejson as json import simplejson as json
import urlparse import urlparse
import urllib
import pycurl import pycurl
import cStringIO import cStringIO
import nilmdb.iteratorizer
class Error(Exception): class Error(Exception):
"""Base exception for both ClientError and ServerError responses""" """Base exception for both ClientError and ServerError responses"""
def __init__(self, def __init__(self,
@@ -60,7 +58,8 @@ class HTTPClient(object):
def _setup_url(self, url = "", params = ""): def _setup_url(self, url = "", params = ""):
url = urlparse.urljoin(self.baseurl, url) url = urlparse.urljoin(self.baseurl, url)
if params: if params:
url = urlparse.urljoin(url, "?" + urllib.urlencode(params, True)) url = urlparse.urljoin(
url, "?" + nilmdb.utils.urllib.urlencode(params, True))
self.curl.setopt(pycurl.URL, url) self.curl.setopt(pycurl.URL, url)
self.url = url self.url = url
@@ -85,6 +84,10 @@ class HTTPClient(object):
raise ClientError(**args) raise ClientError(**args)
else: # pragma: no cover else: # pragma: no cover
if code >= 500 and code <= 599: if code >= 500 and code <= 599:
if args["message"] is None:
args["message"] = ("(no message; try disabling " +
"response.stream option in " +
"nilmdb.server for better debugging)")
raise ServerError(**args) raise ServerError(**args)
else: else:
raise Error(**args) raise Error(**args)
@@ -109,7 +112,7 @@ class HTTPClient(object):
self.curl.setopt(pycurl.WRITEFUNCTION, callback) self.curl.setopt(pycurl.WRITEFUNCTION, callback)
self.curl.perform() self.curl.perform()
try: try:
for i in nilmdb.iteratorizer.Iteratorizer(func): for i in nilmdb.utils.Iteratorizer(func):
if self._status == 200: if self._status == 200:
# If we had a 200 response, yield the data to the caller. # If we had a 200 response, yield the data to the caller.
yield i yield i

View File

@@ -1,8 +1,9 @@
"""Interval and IntervalSet """Interval, IntervalSet
Represents an interval of time, and a set of such intervals. Represents an interval of time, and a set of such intervals.
Intervals are closed, ie. they include timestamps [start, end] Intervals are half-open, ie. they include data points with timestamps
[start, end)
""" """
# First implementation kept a sorted list of intervals and used # First implementation kept a sorted list of intervals and used
@@ -18,16 +19,20 @@ Intervals are closed, ie. they include timestamps [start, end]
# Fourth version is an optimized rb-tree that stores interval starts # Fourth version is an optimized rb-tree that stores interval starts
# and ends directly in the tree, like bxinterval did. # and ends directly in the tree, like bxinterval did.
import rbtree cimport rbtree
cdef extern from "stdint.h":
ctypedef unsigned long long uint64_t
class IntervalError(Exception): class IntervalError(Exception):
"""Error due to interval overlap, etc""" """Error due to interval overlap, etc"""
pass pass
class Interval(object): cdef class Interval:
"""Represents an interval of time.""" """Represents an interval of time."""
def __init__(self, start, end): cdef public double start, end
def __init__(self, double start, double end):
""" """
'start' and 'end' are arbitrary floats that represent time 'start' and 'end' are arbitrary floats that represent time
""" """
@@ -41,9 +46,9 @@ class Interval(object):
return self.__class__.__name__ + "(" + s + ")" return self.__class__.__name__ + "(" + s + ")"
def __str__(self): def __str__(self):
return "[" + str(self.start) + " -> " + str(self.end) + "]" return "[" + repr(self.start) + " -> " + repr(self.end) + ")"
def __cmp__(self, other): def __cmp__(self, Interval other):
"""Compare two intervals. If non-equal, order by start then end""" """Compare two intervals. If non-equal, order by start then end"""
if not isinstance(other, Interval): if not isinstance(other, Interval):
raise TypeError("bad type") raise TypeError("bad type")
@@ -57,20 +62,20 @@ class Interval(object):
return -1 return -1
return 1 return 1
def intersects(self, other): cpdef intersects(self, Interval other):
"""Return True if two Interval objects intersect""" """Return True if two Interval objects intersect"""
if (self.end <= other.start or self.start >= other.end): if (self.end <= other.start or self.start >= other.end):
return False return False
return True return True
def subset(self, start, end): cpdef subset(self, double start, double end):
"""Return a new Interval that is a subset of this one""" """Return a new Interval that is a subset of this one"""
# A subclass that tracks additional data might override this. # A subclass that tracks additional data might override this.
if start < self.start or end > self.end: if start < self.start or end > self.end:
raise IntervalError("not a subset") raise IntervalError("not a subset")
return Interval(start, end) return Interval(start, end)
class DBInterval(Interval): cdef class DBInterval(Interval):
""" """
Like Interval, but also tracks corresponding start/end times and Like Interval, but also tracks corresponding start/end times and
positions within the database. These are not currently modified positions within the database. These are not currently modified
@@ -85,6 +90,9 @@ class DBInterval(Interval):
db_end = 200, db_endpos = 20000 db_end = 200, db_endpos = 20000
""" """
cpdef public double db_start, db_end
cpdef public uint64_t db_startpos, db_endpos
def __init__(self, start, end, def __init__(self, start, end,
db_start, db_end, db_start, db_end,
db_startpos, db_endpos): db_startpos, db_endpos):
@@ -109,7 +117,7 @@ class DBInterval(Interval):
s += ", " + repr(self.db_startpos) + ", " + repr(self.db_endpos) s += ", " + repr(self.db_startpos) + ", " + repr(self.db_endpos)
return self.__class__.__name__ + "(" + s + ")" return self.__class__.__name__ + "(" + s + ")"
def subset(self, start, end): cpdef subset(self, double start, double end):
""" """
Return a new DBInterval that is a subset of this one Return a new DBInterval that is a subset of this one
""" """
@@ -119,11 +127,13 @@ class DBInterval(Interval):
self.db_start, self.db_end, self.db_start, self.db_end,
self.db_startpos, self.db_endpos) self.db_startpos, self.db_endpos)
class IntervalSet(object): cdef class IntervalSet:
""" """
A non-intersecting set of intervals. A non-intersecting set of intervals.
""" """
cdef public rbtree.RBTree tree
def __init__(self, source=None): def __init__(self, source=None):
""" """
'source' is an Interval or IntervalSet to add. 'source' is an Interval or IntervalSet to add.
@@ -148,7 +158,7 @@ class IntervalSet(object):
descs = [ str(x) for x in self ] descs = [ str(x) for x in self ]
return "[" + ", ".join(descs) + "]" return "[" + ", ".join(descs) + "]"
def __eq__(self, other): def __match__(self, other):
# This isn't particularly efficient, but it shouldn't get used in the # This isn't particularly efficient, but it shouldn't get used in the
# general case. # general case.
"""Test equality of two IntervalSets. """Test equality of two IntervalSets.
@@ -167,8 +177,8 @@ class IntervalSet(object):
else: else:
return False return False
this = [ x for x in self ] this = list(self)
that = [ x for x in other ] that = list(other)
try: try:
while True: while True:
@@ -199,10 +209,20 @@ class IntervalSet(object):
except IndexError: except IndexError:
return False return False
def __ne__(self, other): # Use __richcmp__ instead of __eq__, __ne__ for Cython.
return not self.__eq__(other) def __richcmp__(self, other, int op):
if op == 2: # ==
return self.__match__(other)
elif op == 3: # !=
return not self.__match__(other)
return False
#def __eq__(self, other):
# return self.__match__(other)
#
#def __ne__(self, other):
# return not self.__match__(other)
def __iadd__(self, other): def __iadd__(self, object other not None):
"""Inplace add -- modifies self """Inplace add -- modifies self
This throws an exception if the regions being added intersect.""" This throws an exception if the regions being added intersect."""
@@ -210,13 +230,19 @@ class IntervalSet(object):
if self.intersects(other): if self.intersects(other):
raise IntervalError("Tried to add overlapping interval " raise IntervalError("Tried to add overlapping interval "
"to this set") "to this set")
self.tree.insert(rbtree.RBNode(other)) self.tree.insert(rbtree.RBNode(other.start, other.end, other))
else: else:
for x in other: for x in other:
self.__iadd__(x) self.__iadd__(x)
return self return self
def __isub__(self, other): def iadd_nocheck(self, Interval other not None):
"""Inplace add -- modifies self.
'Optimized' version that doesn't check for intersection and
only inserts the new interval into the tree."""
self.tree.insert(rbtree.RBNode(other.start, other.end, other))
def __isub__(self, Interval other not None):
"""Inplace subtract -- modifies self """Inplace subtract -- modifies self
Removes an interval from the set. Must exist exactly Removes an interval from the set. Must exist exactly
@@ -227,13 +253,13 @@ class IntervalSet(object):
self.tree.delete(i) self.tree.delete(i)
return self return self
def __add__(self, other): def __add__(self, other not None):
"""Add -- returns a new object""" """Add -- returns a new object"""
new = IntervalSet(self) new = IntervalSet(self)
new += IntervalSet(other) new += IntervalSet(other)
return new return new
def __and__(self, other): def __and__(self, other not None):
""" """
Compute a new IntervalSet from the intersection of two others Compute a new IntervalSet from the intersection of two others
@@ -244,15 +270,15 @@ class IntervalSet(object):
if not isinstance(other, IntervalSet): if not isinstance(other, IntervalSet):
for i in self.intersection(other): for i in self.intersection(other):
out.tree.insert(rbtree.RBNode(i)) out.tree.insert(rbtree.RBNode(i.start, i.end, i))
else: else:
for x in other: for x in other:
for i in self.intersection(x): for i in self.intersection(x):
out.tree.insert(rbtree.RBNode(i)) out.tree.insert(rbtree.RBNode(i.start, i.end, i))
return out return out
def intersection(self, interval): def intersection(self, Interval interval not None):
""" """
Compute a sequence of intervals that correspond to the Compute a sequence of intervals that correspond to the
intersection between `self` and the provided interval. intersection between `self` and the provided interval.
@@ -269,23 +295,24 @@ class IntervalSet(object):
if i: if i:
if i.start >= interval.start and i.end <= interval.end: if i.start >= interval.start and i.end <= interval.end:
yield i yield i
elif i.start > interval.end:
break
else: else:
subset = i.subset(max(i.start, interval.start), subset = i.subset(max(i.start, interval.start),
min(i.end, interval.end)) min(i.end, interval.end))
yield subset yield subset
def intersects(self, other): cpdef intersects(self, Interval other):
### PROBABLY WRONG
"""Return True if this IntervalSet intersects another interval""" """Return True if this IntervalSet intersects another interval"""
node = self.tree.find_left(other.start, other.end) for n in self.tree.intersect(other.start, other.end):
if node is None: if n.obj.intersects(other):
return False return True
for n in self.tree.inorder(node):
if n.obj:
if n.obj.intersects(other):
return True
if n.obj > other:
break
return False return False
def find_end(self, double t):
"""
Return an Interval from this tree that ends at time t, or
None if it doesn't exist.
"""
n = self.tree.find_left_end(t)
if n and n.obj.end == t:
return n.obj
return None

1
nilmdb/interval.pyxdep Normal file
View File

@@ -0,0 +1 @@
rbtree.pxd

View File

@@ -1,6 +1,5 @@
# cython: profile=False # cython: profile=False
import tables
import time import time
import sys import sys
import inspect import inspect
@@ -122,15 +121,6 @@ class Layout:
s += " %d" % d[i+1] s += " %d" % d[i+1]
return s + "\n" return s + "\n"
# PyTables description
def description(self):
"""Return the PyTables description of this layout"""
desc = {}
desc['timestamp'] = tables.Col.from_type('float64', pos=0)
for n in range(self.count):
desc['c' + str(n+1)] = tables.Col.from_type(self.datatype, pos=n+1)
return tables.Description(desc)
# Get a layout by name # Get a layout by name
def get_named(typestring): def get_named(typestring):
try: try:

View File

@@ -4,17 +4,16 @@
Object that represents a NILM database file. Object that represents a NILM database file.
Manages both the SQL database and the PyTables storage backend. Manages both the SQL database and the table storage backend.
""" """
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py, # Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
# but will pull the nilmdb module instead. # but will pull the nilmdb module instead.
from __future__ import absolute_import from __future__ import absolute_import
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import sqlite3 import sqlite3
import tables
import time import time
import sys import sys
import os import os
@@ -25,6 +24,8 @@ import pyximport
pyximport.install() pyximport.install()
from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError
from . import bulkdata
# Note about performance and transactions: # Note about performance and transactions:
# #
# Committing a transaction in the default sync mode (PRAGMA synchronous=FULL) # Committing a transaction in the default sync mode (PRAGMA synchronous=FULL)
@@ -87,19 +88,13 @@ class StreamError(NilmDBError):
class OverlapError(NilmDBError): class OverlapError(NilmDBError):
pass pass
# Helper that lets us pass a Pytables table into bisect @nilmdb.utils.must_close()
class BisectableTable(object):
def __init__(self, table):
self.table = table
def __getitem__(self, index):
return self.table[index][0]
class NilmDB(object): class NilmDB(object):
verbose = 0 verbose = 0
def __init__(self, basepath, sync=True, max_results=None): def __init__(self, basepath, sync=True, max_results=None):
# set up path # set up path
self.basepath = os.path.abspath(basepath.rstrip('/')) self.basepath = os.path.abspath(basepath)
# Create the database path if it doesn't exist # Create the database path if it doesn't exist
try: try:
@@ -108,16 +103,16 @@ class NilmDB(object):
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
raise IOError("can't create tree " + self.basepath) raise IOError("can't create tree " + self.basepath)
# Our HD5 file goes inside it # Our data goes inside it
h5filename = os.path.abspath(self.basepath + "/data.h5") self.data = bulkdata.BulkData(self.basepath)
self.h5file = tables.openFile(h5filename, "a", "NILM Database")
# SQLite database too # SQLite database too
sqlfilename = os.path.abspath(self.basepath + "/data.sql") sqlfilename = os.path.join(self.basepath, "data.sql")
# We use check_same_thread = False, assuming that the rest # We use check_same_thread = False, assuming that the rest
# of the code (e.g. Server) will be smart and not access this # of the code (e.g. Server) will be smart and not access this
# database from multiple threads simultaneously. That requirement # database from multiple threads simultaneously. Otherwise
# may be relaxed later. # false positives will occur when the database is only opened
# in one thread, and only accessed in another.
self.con = sqlite3.connect(sqlfilename, check_same_thread = False) self.con = sqlite3.connect(sqlfilename, check_same_thread = False)
self._sql_schema_update() self._sql_schema_update()
@@ -134,17 +129,6 @@ class NilmDB(object):
else: else:
self.max_results = 16384 self.max_results = 16384
self.opened = True
# Cached intervals
self._cached_iset = {}
def __del__(self):
if "opened" in self.__dict__: # pragma: no cover
fprintf(sys.stderr,
"error: NilmDB.close() wasn't called, path %s",
self.basepath)
def get_basepath(self): def get_basepath(self):
return self.basepath return self.basepath
@@ -152,8 +136,7 @@ class NilmDB(object):
if self.con: if self.con:
self.con.commit() self.con.commit()
self.con.close() self.con.close()
self.h5file.close() self.data.close()
del self.opened
def _sql_schema_update(self): def _sql_schema_update(self):
cur = self.con.cursor() cur = self.con.cursor()
@@ -170,58 +153,78 @@ class NilmDB(object):
with self.con: with self.con:
cur.execute("PRAGMA user_version = {v:d}".format(v=version)) cur.execute("PRAGMA user_version = {v:d}".format(v=version))
@nilmdb.utils.lru_cache(size = 16)
def _get_intervals(self, stream_id): def _get_intervals(self, stream_id):
""" """
Return a mutable IntervalSet corresponding to the given stream ID. Return a mutable IntervalSet corresponding to the given stream ID.
""" """
# Load from database if not cached iset = IntervalSet()
if stream_id not in self._cached_iset: result = self.con.execute("SELECT start_time, end_time, "
iset = IntervalSet() "start_pos, end_pos "
result = self.con.execute("SELECT start_time, end_time, " "FROM ranges "
"start_pos, end_pos " "WHERE stream_id=?", (stream_id,))
"FROM ranges " try:
"WHERE stream_id=?", (stream_id,)) for (start_time, end_time, start_pos, end_pos) in result:
try: iset += DBInterval(start_time, end_time,
for (start_time, end_time, start_pos, end_pos) in result: start_time, end_time,
iset += DBInterval(start_time, end_time, start_pos, end_pos)
start_time, end_time, except IntervalError as e: # pragma: no cover
start_pos, end_pos) raise NilmDBError("unexpected overlap in ranges table!")
except IntervalError as e: # pragma: no cover
raise NilmDBError("unexpected overlap in ranges table!")
self._cached_iset[stream_id] = iset
# Return cached value
return self._cached_iset[stream_id]
# TODO: Split add_interval into two pieces, one to add return iset
# and one to flush to disk?
# Need to think about this. Basic problem is that we can't
# mess with intervals once they're in the IntervalSet,
# without mucking with bxinterval internals.
# Maybe add a separate optimization step?
# Join intervals that have a fairly small gap between them
def _add_interval(self, stream_id, interval, start_pos, end_pos): def _add_interval(self, stream_id, interval, start_pos, end_pos):
""" """
Add interval to the internal interval cache, and to the database. Add interval to the internal interval cache, and to the database.
Note: arguments must be ints (not numpy.int64, etc) Note: arguments must be ints (not numpy.int64, etc)
""" """
# Ensure this stream's intervals are cached, and add the new # Load this stream's intervals
# interval to that cache.
iset = self._get_intervals(stream_id) iset = self._get_intervals(stream_id)
try:
iset += DBInterval(interval.start, interval.end, # Check for overlap
interval.start, interval.end, if iset.intersects(interval): # pragma: no cover (gets caught earlier)
start_pos, end_pos)
except IntervalError as e: # pragma: no cover
raise NilmDBError("new interval overlaps existing data") raise NilmDBError("new interval overlaps existing data")
# Check for adjacency. If there's a stream in the database
# that ends exactly when this one starts, and the database
# rows match up, we can make one interval that covers the
# time range [adjacent.start -> interval.end)
# and database rows [ adjacent.start_pos -> end_pos ].
# Only do this if the resulting interval isn't too large.
max_merged_rows = 30000000 # a bit more than 1 hour at 8 KHz
adjacent = iset.find_end(interval.start)
if (adjacent is not None and
start_pos == adjacent.db_endpos and
(end_pos - adjacent.db_startpos) < max_merged_rows):
# First delete the old one, both from our iset and the
# database
iset -= adjacent
self.con.execute("DELETE FROM ranges WHERE "
"stream_id=? AND start_time=? AND "
"end_time=? AND start_pos=? AND "
"end_pos=?", (stream_id,
adjacent.db_start,
adjacent.db_end,
adjacent.db_startpos,
adjacent.db_endpos))
# Now update our interval so the fallthrough add is
# correct.
interval.start = adjacent.start
start_pos = adjacent.db_startpos
# Add the new interval to the iset
iset.iadd_nocheck(DBInterval(interval.start, interval.end,
interval.start, interval.end,
start_pos, end_pos))
# Insert into the database # Insert into the database
self.con.execute("INSERT INTO ranges " self.con.execute("INSERT INTO ranges "
"(stream_id,start_time,end_time,start_pos,end_pos) " "(stream_id,start_time,end_time,start_pos,end_pos) "
"VALUES (?,?,?,?,?)", "VALUES (?,?,?,?,?)",
(stream_id, interval.start, interval.end, (stream_id, interval.start, interval.end,
int(start_pos), int(end_pos))) int(start_pos), int(end_pos)))
self.con.commit() self.con.commit()
def stream_list(self, path = None, layout = None): def stream_list(self, path = None, layout = None):
@@ -285,38 +288,11 @@ class NilmDB(object):
layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8' layout_name: string for nilmdb.layout.get_named(), e.g. 'float32_8'
""" """
if path[0] != '/': # Create the bulk storage. Raises ValueError on error, which we
raise ValueError("paths must start with /") # pass along.
[ group, node ] = path.rsplit("/", 1) self.data.create(path, layout_name)
if group == '':
raise ValueError("invalid path")
# Make the group structure, one element at a time # Insert into SQL database once the bulk storage is happy
group_path = group.lstrip('/').split("/")
for i in range(len(group_path)):
parent = "/" + "/".join(group_path[0:i])
child = group_path[i]
try:
self.h5file.createGroup(parent, child)
except tables.NodeError:
pass
# Get description
try:
desc = nilmdb.layout.get_named(layout_name).description()
except KeyError:
raise ValueError("no such layout")
# Estimated table size (for PyTables optimization purposes): assume
# 3 months worth of data at 8 KHz. It's OK if this is wrong.
exp_rows = 8000 * 60*60*24*30*3
# Create the table
table = self.h5file.createTable(group, node,
description = desc,
expectedrows = exp_rows)
# Insert into SQL database once the PyTables is happy
with self.con as con: with self.con as con:
con.execute("INSERT INTO streams (path, layout) VALUES (?,?)", con.execute("INSERT INTO streams (path, layout) VALUES (?,?)",
(path, layout_name)) (path, layout_name))
@@ -337,8 +313,7 @@ class NilmDB(object):
""" """
stream_id = self._stream_id(path) stream_id = self._stream_id(path)
with self.con as con: with self.con as con:
con.execute("DELETE FROM metadata " con.execute("DELETE FROM metadata WHERE stream_id=?", (stream_id,))
"WHERE stream_id=?", (stream_id,))
for key in data: for key in data:
if data[key] != '': if data[key] != '':
con.execute("INSERT INTO metadata VALUES (?, ?, ?)", con.execute("INSERT INTO metadata VALUES (?, ?, ?)",
@@ -361,44 +336,47 @@ class NilmDB(object):
data.update(newdata) data.update(newdata)
self.stream_set_metadata(path, data) self.stream_set_metadata(path, data)
def stream_insert(self, path, parser, old_timestamp = None): def stream_destroy(self, path):
"""Fully remove a table and all of its data from the database.
No way to undo it! Metadata is removed."""
stream_id = self._stream_id(path)
# Delete the cached interval data
self._get_intervals.cache_remove(self, stream_id)
# Delete the data
self.data.destroy(path)
# Delete metadata, stream, intervals
with self.con as con:
con.execute("DELETE FROM metadata WHERE stream_id=?", (stream_id,))
con.execute("DELETE FROM ranges WHERE stream_id=?", (stream_id,))
con.execute("DELETE FROM streams WHERE id=?", (stream_id,))
def stream_insert(self, path, start, end, data):
"""Insert new data into the database. """Insert new data into the database.
path: Path at which to add the data path: Path at which to add the data
parser: nilmdb.layout.Parser instance full of data to insert start: Starting timestamp
end: Ending timestamp
data: Rows of data, to be passed to PyTable's table.append
method. E.g. nilmdb.layout.Parser.data
""" """
if (not parser.min_timestamp or not parser.max_timestamp or
not len(parser.data)):
raise StreamError("no data provided")
# If we were provided with an old timestamp, the expectation
# is that the client has a contiguous block of time it is sending,
# but it's doing it over multiple calls to stream_insert.
# old_timestamp is the max_timestamp of the previous insert.
# To make things continuous, use that as our starting timestamp
# instead of what the parser found.
if old_timestamp:
min_timestamp = old_timestamp
else:
min_timestamp = parser.min_timestamp
# First check for basic overlap using timestamp info given. # First check for basic overlap using timestamp info given.
stream_id = self._stream_id(path) stream_id = self._stream_id(path)
iset = self._get_intervals(stream_id) iset = self._get_intervals(stream_id)
interval = Interval(min_timestamp, parser.max_timestamp) interval = Interval(start, end)
if iset.intersects(interval): if iset.intersects(interval):
raise OverlapError("new data overlaps existing data: " raise OverlapError("new data overlaps existing data at range: "
+ str(iset & interval)) + str(iset & interval))
# Insert the data into pytables # Insert the data
table = self.h5file.getNode(path) table = self.data.getnode(path)
row_start = table.nrows row_start = table.nrows
table.append(parser.data) table.append(data)
row_end = table.nrows row_end = table.nrows
table.flush()
# Insert the record into the sql database. # Insert the record into the sql database.
# Casts are to convert from numpy.int64. self._add_interval(stream_id, interval, row_start, row_end)
self._add_interval(stream_id, interval, int(row_start), int(row_end))
# And that's all # And that's all
return "ok" return "ok"
@@ -413,7 +391,7 @@ class NilmDB(object):
# Optimization for the common case where an interval wasn't truncated # Optimization for the common case where an interval wasn't truncated
if interval.start == interval.db_start: if interval.start == interval.db_start:
return interval.db_startpos return interval.db_startpos
return bisect.bisect_left(BisectableTable(table), return bisect.bisect_left(bulkdata.TimestampOnlyTable(table),
interval.start, interval.start,
interval.db_startpos, interval.db_startpos,
interval.db_endpos) interval.db_endpos)
@@ -432,7 +410,7 @@ class NilmDB(object):
# want to include the given timestamp in the results. This is # want to include the given timestamp in the results. This is
# so a queries like 1:00 -> 2:00 and 2:00 -> 3:00 return # so a queries like 1:00 -> 2:00 and 2:00 -> 3:00 return
# non-overlapping data. # non-overlapping data.
return bisect.bisect_left(BisectableTable(table), return bisect.bisect_left(bulkdata.TimestampOnlyTable(table),
interval.end, interval.end,
interval.db_startpos, interval.db_startpos,
interval.db_endpos) interval.db_endpos)
@@ -456,7 +434,7 @@ class NilmDB(object):
than actually fetching the data. It is not limited by than actually fetching the data. It is not limited by
max_results. max_results.
""" """
table = self.h5file.getNode(path) table = self.data.getnode(path)
stream_id = self._stream_id(path) stream_id = self._stream_id(path)
intervals = self._get_intervals(stream_id) intervals = self._get_intervals(stream_id)
requested = Interval(start or 0, end or 1e12) requested = Interval(start or 0, end or 1e12)

23
nilmdb/rbtree.pxd Normal file
View File

@@ -0,0 +1,23 @@
cdef class RBNode:
cdef public object obj
cdef public double start, end
cdef public int red
cdef public RBNode left, right, parent
cdef class RBTree:
cdef public RBNode nil, root
cpdef getroot(RBTree self)
cdef void __rotate_left(RBTree self, RBNode x)
cdef void __rotate_right(RBTree self, RBNode y)
cdef RBNode __successor(RBTree self, RBNode x)
cpdef RBNode successor(RBTree self, RBNode x)
cdef RBNode __predecessor(RBTree self, RBNode x)
cpdef RBNode predecessor(RBTree self, RBNode x)
cpdef insert(RBTree self, RBNode z)
cdef void __insert_fixup(RBTree self, RBNode x)
cpdef delete(RBTree self, RBNode z)
cdef inline void __delete_fixup(RBTree self, RBNode x)
cpdef RBNode find(RBTree self, double start, double end)
cpdef RBNode find_left_end(RBTree self, double t)
cpdef RBNode find_right_start(RBTree self, double t)

View File

@@ -1,20 +1,27 @@
"""Red-black tree, where keys are stored as start/end timestamps.""" # cython: profile=False
# cython: cdivision=True
"""
Jim Paris <jim@jtan.com>
Red-black tree, where keys are stored as start/end timestamps.
This is a basic interval tree that holds half-open intervals:
[start, end)
Intervals must not overlap. Fixing that would involve making this
into an augmented interval tree as described in CLRS 14.3.
Code that assumes non-overlapping intervals is marked with the
string 'non-overlapping'.
"""
import sys import sys
cimport rbtree
class RBNode(object): cdef class RBNode:
"""One node of the Red/Black tree. obj points to any object, """One node of the Red/Black tree, containing a key (start, end)
'start' and 'end' are timestamps that represent the key.""" and value (obj)"""
def __init__(self, obj = None, start = None, end = None): def __init__(self, double start, double end, object obj = None):
"""If given an object but no start/end times, get the
start/end times from the object.
If given start/end times, obj can be anything, including None."""
self.obj = obj self.obj = obj
if start is None:
start = obj.start
if end is None:
end = obj.end
self.start = start self.start = start
self.end = end self.end = end
self.red = False self.red = False
@@ -26,21 +33,23 @@ class RBNode(object):
color = "R" color = "R"
else: else:
color = "B" color = "B"
return ("[node " if self.start == sys.float_info.min:
return "[node nil]"
return ("[node ("
+ str(self.obj) + ") "
+ str(self.start) + " -> " + str(self.end) + " " + str(self.start) + " -> " + str(self.end) + " "
+ color + "]") + color + "]")
class RBTree(object): cdef class RBTree:
"""Red/Black tree""" """Red/Black tree"""
# Init # Init
def __init__(self): def __init__(self):
self.nil = RBNode(start = sys.float_info.min, self.nil = RBNode(start = sys.float_info.min,
end = sys.float_info.min) end = sys.float_info.min)
self.nil.left = self.nil self.nil.left = self.nil
self.nil.right = self.nil self.nil.right = self.nil
self.nil.parent = self.nil self.nil.parent = self.nil
self.nil.nil = True
self.root = RBNode(start = sys.float_info.max, self.root = RBNode(start = sys.float_info.max,
end = sys.float_info.max) end = sys.float_info.max)
@@ -48,9 +57,21 @@ class RBTree(object):
self.root.right = self.nil self.root.right = self.nil
self.root.parent = self.nil self.root.parent = self.nil
# We have a dummy root node to simplify operations, so from an
# external point of view, its left child is the real root.
cpdef getroot(self):
return self.root.left
# Rotations and basic operations # Rotations and basic operations
def __rotate_left(self, x): cdef void __rotate_left(self, RBNode x):
y = x.right """Rotate left:
# x y
# / \ --> / \
# z y x w
# / \ / \
# v w z v
"""
cdef RBNode y = x.right
x.right = y.left x.right = y.left
if y.left is not self.nil: if y.left is not self.nil:
y.left.parent = x y.left.parent = x
@@ -62,8 +83,15 @@ class RBTree(object):
y.left = x y.left = x
x.parent = y x.parent = y
def __rotate_right(self, y): cdef void __rotate_right(self, RBNode y):
x = y.left """Rotate right:
# y x
# / \ --> / \
# x w z y
# / \ / \
# z v v w
"""
cdef RBNode x = y.left
y.left = x.right y.left = x.right
if x.right is not self.nil: if x.right is not self.nil:
x.right.parent = y x.right.parent = y
@@ -75,9 +103,9 @@ class RBTree(object):
x.right = y x.right = y
y.parent = x y.parent = x
def __successor(self, x): cdef RBNode __successor(self, RBNode x):
"""Returns the successor of RBNode x""" """Returns the successor of RBNode x"""
y = x.right cdef RBNode y = x.right
if y is not self.nil: if y is not self.nil:
while y.left is not self.nil: while y.left is not self.nil:
y = y.left y = y.left
@@ -89,10 +117,14 @@ class RBTree(object):
if y is self.root: if y is self.root:
return self.nil return self.nil
return y return y
cpdef RBNode successor(self, RBNode x):
"""Returns the successor of RBNode x, or None"""
cdef RBNode y = self.__successor(x)
return y if y is not self.nil else None
def _predecessor(self, x): cdef RBNode __predecessor(self, RBNode x):
"""Returns the predecessor of RBNode x""" """Returns the predecessor of RBNode x"""
y = x.left cdef RBNode y = x.left
if y is not self.nil: if y is not self.nil:
while y.right is not self.nil: while y.right is not self.nil:
y = y.right y = y.right
@@ -105,14 +137,18 @@ class RBTree(object):
x = y x = y
y = y.parent y = y.parent
return y return y
cpdef RBNode predecessor(self, RBNode x):
"""Returns the predecessor of RBNode x, or None"""
cdef RBNode y = self.__predecessor(x)
return y if y is not self.nil else None
# Insertion # Insertion
def insert(self, z): cpdef insert(self, RBNode z):
"""Insert RBNode z into RBTree and rebalance as necessary""" """Insert RBNode z into RBTree and rebalance as necessary"""
z.left = self.nil z.left = self.nil
z.right = self.nil z.right = self.nil
y = self.root cdef RBNode y = self.root
x = self.root.left cdef RBNode x = self.root.left
while x is not self.nil: while x is not self.nil:
y = x y = x
if (x.start > z.start or (x.start == z.start and x.end > z.end)): if (x.start > z.start or (x.start == z.start and x.end > z.end)):
@@ -128,7 +164,7 @@ class RBTree(object):
# relabel/rebalance # relabel/rebalance
self.__insert_fixup(z) self.__insert_fixup(z)
def __insert_fixup(self, x): cdef void __insert_fixup(self, RBNode x):
"""Rebalance/fix RBTree after a simple insertion of RBNode x""" """Rebalance/fix RBTree after a simple insertion of RBNode x"""
x.red = True x.red = True
while x.parent.red: while x.parent.red:
@@ -163,10 +199,11 @@ class RBTree(object):
self.root.left.red = False self.root.left.red = False
# Deletion # Deletion
def delete(self, z): cpdef delete(self, RBNode z):
if z.left is None or z.right is None: if z.left is None or z.right is None:
raise AttributeError("you can only delete a node object " raise AttributeError("you can only delete a node object "
+ "from the tree; use find() to get one") + "from the tree; use find() to get one")
cdef RBNode x, y
if z.left is self.nil or z.right is self.nil: if z.left is self.nil or z.right is self.nil:
y = z y = z
else: else:
@@ -203,10 +240,10 @@ class RBTree(object):
if not y.red: if not y.red:
self.__delete_fixup(x) self.__delete_fixup(x)
def __delete_fixup(self, x): cdef void __delete_fixup(self, RBNode x):
"""Rebalance/fix RBTree after a deletion. RBNode x is the """Rebalance/fix RBTree after a deletion. RBNode x is the
child of the spliced out node.""" child of the spliced out node."""
rootLeft = self.root.left cdef RBNode rootLeft = self.root.left
while not x.red and x is not rootLeft: while not x.red and x is not rootLeft:
if x is x.parent.left: if x is x.parent.left:
w = x.parent.right w = x.parent.right
@@ -252,141 +289,89 @@ class RBTree(object):
x = rootLeft # exit loop x = rootLeft # exit loop
x.red = False x.red = False
# Rendering
def __render_dot_node(self, node, max_depth = 20):
from printf import sprintf
"""Render a single node and its children into a dot graph fragment"""
if max_depth == 0:
return ""
if node is self.nil:
return ""
def c(red):
if red:
return 'color="#ff0000", style=filled, fillcolor="#ffc0c0"'
else:
return 'color="#000000", style=filled, fillcolor="#c0c0c0"'
s = sprintf("%d [label=\"%g\\n%g\", %s];\n",
id(node),
node.start, node.end,
c(node.red))
if node.left is self.nil:
s += sprintf("L%d [label=\"-\", %s];\n", id(node), c(False))
s += sprintf("%d -> L%d [label=L];\n", id(node), id(node))
else:
s += sprintf("%d -> %d [label=L];\n", id(node), id(node.left))
if node.right is self.nil:
s += sprintf("R%d [label=\"-\", %s];\n", id(node), c(False))
s += sprintf("%d -> R%d [label=R];\n", id(node), id(node))
else:
s += sprintf("%d -> %d [label=R];\n", id(node), id(node.right))
s += self.__render_dot_node(node.left, max_depth-1)
s += self.__render_dot_node(node.right, max_depth-1)
return s
def render_dot(self, title = "RBTree"):
"""Render the entire RBTree as a dot graph"""
return ("digraph rbtree {\n"
+ self.__render_dot_node(self.root.left)
+ "}\n");
def render_dot_live(self, title = "RBTree"):
"""Render the entire RBTree as a dot graph, live GTK view"""
import gtk
import gtk.gdk
sys.path.append("/usr/share/xdot")
import xdot
xdot.Pen.highlighted = lambda pen: pen
s = ("digraph rbtree {\n"
+ self.__render_dot_node(self.root)
+ "}\n");
window = xdot.DotWindow()
window.set_dotcode(s)
window.set_title(title + " - any key to close")
window.connect('destroy', gtk.main_quit)
def quit(widget, event):
if not event.is_modifier:
window.destroy()
gtk.main_quit()
window.widget.connect('key-press-event', quit)
gtk.main()
# Walking, searching # Walking, searching
def __iter__(self): def __iter__(self):
return self.inorder(self.root.left) return self.inorder()
def inorder(self, x = None): def inorder(self, RBNode x = None):
"""Generator that performs an inorder walk for the tree """Generator that performs an inorder walk for the tree
starting at RBNode x""" rooted at RBNode x"""
if x is None: if x is None:
x = self.root.left x = self.getroot()
while x.left is not self.nil: while x.left is not self.nil:
x = x.left x = x.left
while x is not self.nil: while x is not self.nil:
yield x yield x
x = self.__successor(x) x = self.__successor(x)
def __find_all(self, start, end, x): cpdef RBNode find(self, double start, double end):
"""Find node with the specified (start,end) key. """Return the node with exactly the given start and end."""
Also returns the largest node less than or equal to key, cdef RBNode x = self.getroot()
and the smallest node greater or equal to than key."""
if x is None:
x = self.root.left
largest = self.nil
smallest = self.nil
while x is not self.nil: while x is not self.nil:
if start < x.start: if start < x.start:
smallest = x x = x.left
x = x.left # start <
elif start == x.start: elif start == x.start:
if end < x.end: if end == x.end:
smallest = x break # found it
x = x.left # start =, end < elif end < x.end:
elif end == x.end: # found it x = x.left
smallest = x
largest = x
break
else: else:
largest = x x = x.right
x = x.right # start =, end >
else: else:
largest = x x = x.right
x = x.right # start > return x if x is not self.nil else None
return (x, smallest, largest)
def find(self, start, end, x = None): cpdef RBNode find_left_end(self, double t):
"""Find node with the key == (start,end), or None""" """Find the leftmode node with end >= t. With non-overlapping
y = self.__find_all(start, end, x)[1] intervals, this is the first node that might overlap time t.
return y if y is not self.nil else None
def find_right(self, start, end, x = None): Note that this relies on non-overlapping intervals, since
"""Find node with the smallest key >= (start,end), or None""" it assumes that we can use the endpoints to traverse the
y = self.__find_all(start, end, x)[1] tree even though it was created using the start points."""
return y if y is not self.nil else None cdef RBNode x = self.getroot()
while x is not self.nil:
if t < x.end:
if x.left is self.nil:
break
x = x.left
elif t == x.end:
break
else:
if x.right is self.nil:
x = self.__successor(x)
break
x = x.right
return x if x is not self.nil else None
def find_left(self, start, end, x = None): cpdef RBNode find_right_start(self, double t):
"""Find node with the largest key <= (start,end), or None""" """Find the rightmode node with start <= t. With non-overlapping
y = self.__find_all(start, end, x)[2] intervals, this is the last node that might overlap time t."""
return y if y is not self.nil else None cdef RBNode x = self.getroot()
while x is not self.nil:
if t < x.start:
if x.left is self.nil:
x = self.__predecessor(x)
break
x = x.left
elif t == x.start:
break
else:
if x.right is self.nil:
break
x = x.right
return x if x is not self.nil else None
# Intersections # Intersections
def intersect(self, start, end): def intersect(self, double start, double end):
"""Generator that returns nodes that overlap the given """Generator that returns nodes that overlap the given
(start,end) range, for the tree rooted at RBNode x. (start,end) range. Assumes non-overlapping intervals."""
# Start with the leftmode node that ends after start
NOTE: this assumes non-overlapping intervals.""" cdef RBNode n = self.find_left_end(start)
# Start with the leftmost node before the starting point while n is not None:
n = self.find_left(start, start) if n.start >= end:
# If we didn't find one, look for the leftmode node before the # this node starts after the requested end; we're done
# ending point instead. break
if n is None: if start < n.end:
n = self.find_left(end, end) # this node overlaps our requested area
# If we still didn't find it, there are no intervals that intersect. yield n
if n is None: n = self.successor(n)
return none
# Now yield this node and all successors until their endpoints
if False:
yield
return

1
nilmdb/rbtree.pyxdep Normal file
View File

@@ -0,0 +1 @@
rbtree.pxd

View File

@@ -3,15 +3,15 @@
# Need absolute_import so that "import nilmdb" won't pull in nilmdb.py, # Need absolute_import so that "import nilmdb" won't pull in nilmdb.py,
# but will pull the nilmdb module instead. # but will pull the nilmdb module instead.
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.utils.printf import *
import nilmdb import nilmdb
from nilmdb.printf import *
import cherrypy import cherrypy
import sys import sys
import time import time
import os import os
import simplejson as json import simplejson as json
import functools
try: try:
import cherrypy import cherrypy
@@ -26,6 +26,46 @@ class NilmApp(object):
version = "1.1" version = "1.1"
# Decorators
def chunked_response(func):
"""Decorator to enable chunked responses"""
# Set this to False to get better tracebacks from some requests
# (/stream/extract, /stream/intervals).
func._cp_config = { 'response.stream': True }
return func
def workaround_cp_bug_1200(func): # pragma: no cover (just a workaround)
"""Decorator to work around CherryPy bug #1200 in a response
generator"""
# Even if chunked responses are disabled, you may still miss miss
# LookupError, or UnicodeError exceptions due to CherryPy bug
# #1200. This throws them as generic Exceptions insteads.
import traceback
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
for val in func(*args, **kwargs):
yield val
except (LookupError, UnicodeError) as e:
raise Exception("bug workaround; real exception is:\n" +
traceback.format_exc())
return wrapper
def exception_to_httperror(response = "400 Bad Request"):
"""Return a decorator that catches Exception and throws
a HTTPError describing it instead"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
message = sprintf("%s: %s", type(e).__name__, str(e))
raise cherrypy.HTTPError(response, message)
return wrapper
return decorator
# CherryPy apps
class Root(NilmApp): class Root(NilmApp):
"""Root application for NILM database""" """Root application for NILM database"""
@@ -59,7 +99,7 @@ class Root(NilmApp):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
def dbsize(self): def dbsize(self):
return nilmdb.du.du(self.db.get_basepath()) return nilmdb.utils.du(self.db.get_basepath())
class Stream(NilmApp): class Stream(NilmApp):
"""Stream-specific operations""" """Stream-specific operations"""
@@ -78,15 +118,20 @@ class Stream(NilmApp):
# /stream/create?path=/newton/prep&layout=PrepData # /stream/create?path=/newton/prep&layout=PrepData
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror()
def create(self, path, layout): def create(self, path, layout):
"""Create a new stream in the database. Provide path """Create a new stream in the database. Provide path
and one of the nilmdb.layout.layouts keys. and one of the nilmdb.layout.layouts keys.
""" """
try: return self.db.stream_create(path, layout)
return self.db.stream_create(path, layout)
except Exception as e: # /stream/destroy?path=/newton/prep
message = sprintf("%s: %s", type(e).__name__, e.message) @cherrypy.expose
raise cherrypy.HTTPError("400 Bad Request", message) @cherrypy.tools.json_out()
@exception_to_httperror()
def destroy(self, path):
"""Delete a stream and its associated data."""
return self.db.stream_destroy(path)
# /stream/get_metadata?path=/newton/prep # /stream/get_metadata?path=/newton/prep
# /stream/get_metadata?path=/newton/prep&key=foo&key=bar # /stream/get_metadata?path=/newton/prep&key=foo&key=bar
@@ -115,49 +160,35 @@ class Stream(NilmApp):
# /stream/set_metadata?path=/newton/prep&data=<json> # /stream/set_metadata?path=/newton/prep&data=<json>
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror()
def set_metadata(self, path, data): def set_metadata(self, path, data):
"""Set metadata for the named stream, replacing any """Set metadata for the named stream, replacing any
existing metadata. Data should be a json-encoded existing metadata. Data should be a json-encoded
dictionary""" dictionary"""
try: data_dict = json.loads(data)
data_dict = json.loads(data) self.db.stream_set_metadata(path, data_dict)
self.db.stream_set_metadata(path, data_dict)
except Exception as e:
message = sprintf("%s: %s", type(e).__name__, e.message)
raise cherrypy.HTTPError("400 Bad Request", message)
return "ok" return "ok"
# /stream/update_metadata?path=/newton/prep&data=<json> # /stream/update_metadata?path=/newton/prep&data=<json>
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@exception_to_httperror()
def update_metadata(self, path, data): def update_metadata(self, path, data):
"""Update metadata for the named stream. Data """Update metadata for the named stream. Data
should be a json-encoded dictionary""" should be a json-encoded dictionary"""
try: data_dict = json.loads(data)
data_dict = json.loads(data) self.db.stream_update_metadata(path, data_dict)
self.db.stream_update_metadata(path, data_dict)
except Exception as e:
message = sprintf("%s: %s", type(e).__name__, e.message)
raise cherrypy.HTTPError("400 Bad Request", message)
return "ok" return "ok"
# /stream/insert?path=/newton/prep # /stream/insert?path=/newton/prep
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
#@cherrypy.tools.disable_prb() #@cherrypy.tools.disable_prb()
def insert(self, path, old_timestamp = None): def insert(self, path, start, end):
""" """
Insert new data into the database. Provide textual data Insert new data into the database. Provide textual data
(matching the path's layout) as a HTTP PUT. (matching the path's layout) as a HTTP PUT.
old_timestamp is used when making multiple, split-up insertions
for a larger contiguous block of data. The first insert
will return the maximum timestamp that it saw, and the second
insert should provide this timestamp as an argument. This is
used to extend the previous database interval rather than
start a new one.
""" """
# 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.
# However, CherryPy 3.2.2 has a bug where this fails for GET # However, CherryPy 3.2.2 has a bug where this fails for GET
@@ -182,22 +213,36 @@ class Stream(NilmApp):
"Error parsing input data: " + "Error parsing input data: " +
e.message) e.message)
if (not parser.min_timestamp or not parser.max_timestamp or
not len(parser.data)):
raise cherrypy.HTTPError("400 Bad Request",
"no data provided")
# Check limits
start = float(start)
end = float(end)
if parser.min_timestamp < start:
raise cherrypy.HTTPError("400 Bad Request", "Data timestamp " +
repr(parser.min_timestamp) +
" < start time " + repr(start))
if parser.max_timestamp >= end:
raise cherrypy.HTTPError("400 Bad Request", "Data timestamp " +
repr(parser.max_timestamp) +
" >= end time " + repr(end))
# Now do the nilmdb insert, passing it the parser full of data. # Now do the nilmdb insert, passing it the parser full of data.
try: try:
if old_timestamp: result = self.db.stream_insert(path, start, end, parser.data)
old_timestamp = float(old_timestamp)
result = self.db.stream_insert(path, parser, old_timestamp)
except nilmdb.nilmdb.NilmDBError as e: except nilmdb.nilmdb.NilmDBError as e:
raise cherrypy.HTTPError("400 Bad Request", e.message) raise cherrypy.HTTPError("400 Bad Request", e.message)
# Return the maximum timestamp that we saw. The client will # Done
# return this back to us as the old_timestamp parameter, if return "ok"
# it has more data to send.
return ("ok", parser.max_timestamp)
# /stream/intervals?path=/newton/prep # /stream/intervals?path=/newton/prep
# /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0 # /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0
@cherrypy.expose @cherrypy.expose
@chunked_response
def intervals(self, path, start = None, end = None): def intervals(self, path, start = None, end = None):
""" """
Get intervals from backend database. Streams the resulting Get intervals from backend database. Streams the resulting
@@ -219,9 +264,9 @@ class Stream(NilmApp):
if len(streams) != 1: if len(streams) != 1:
raise cherrypy.HTTPError("404 Not Found", "No such stream") raise cherrypy.HTTPError("404 Not Found", "No such stream")
@workaround_cp_bug_1200
def content(start, end): def content(start, end):
# Note: disable response.stream below to get better debug info # Note: disable chunked responses to see tracebacks from here.
# from tracebacks in this subfunction.
while True: while True:
(intervals, restart) = self.db.stream_intervals(path,start,end) (intervals, restart) = self.db.stream_intervals(path,start,end)
response = ''.join([ json.dumps(i) + "\n" for i in intervals ]) response = ''.join([ json.dumps(i) + "\n" for i in intervals ])
@@ -230,10 +275,10 @@ class Stream(NilmApp):
break break
start = restart start = restart
return content(start, end) return content(start, end)
intervals._cp_config = { 'response.stream': True } # chunked HTTP response
# /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0 # /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0
@cherrypy.expose @cherrypy.expose
@chunked_response
def extract(self, path, start = None, end = None, count = False): def extract(self, path, start = None, end = None, count = False):
""" """
Extract data from backend database. Streams the resulting Extract data from backend database. Streams the resulting
@@ -263,9 +308,9 @@ class Stream(NilmApp):
# Get formatter # Get formatter
formatter = nilmdb.layout.Formatter(layout) formatter = nilmdb.layout.Formatter(layout)
@workaround_cp_bug_1200
def content(start, end, count): def content(start, end, count):
# Note: disable response.stream below to get better debug info # Note: disable chunked responses to see tracebacks from here.
# from tracebacks in this subfunction.
if count: if count:
matched = self.db.stream_extract(path, start, end, count) matched = self.db.stream_extract(path, start, end, count)
yield sprintf("%d\n", matched) yield sprintf("%d\n", matched)
@@ -281,8 +326,6 @@ class Stream(NilmApp):
return return
start = restart start = restart
return content(start, end, count) return content(start, end, count)
extract._cp_config = { 'response.stream': True } # chunked HTTP response
class Exiter(object): class Exiter(object):
"""App that exits the server, for testing""" """App that exits the server, for testing"""
@@ -307,7 +350,7 @@ class Server(object):
# Need to wrap DB object in a serializer because we'll call # Need to wrap DB object in a serializer because we'll call
# into it from separate threads. # into it from separate threads.
self.embedded = embedded self.embedded = embedded
self.db = nilmdb.serializer.WrapObject(db) self.db = nilmdb.utils.Serializer(db)
cherrypy.config.update({ cherrypy.config.update({
'server.socket_host': host, 'server.socket_host': host,
'server.socket_port': port, 'server.socket_port': port,

View File

@@ -1,7 +1,7 @@
"""File-like objects that add timestamps to the input lines""" """File-like objects that add timestamps to the input lines"""
from __future__ import absolute_import from __future__ import absolute_import
from nilmdb.printf import * from nilmdb.utils.printf import *
import time import time
import os import os

9
nilmdb/utils/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""NilmDB utilities"""
from .timer import Timer
from .iteratorizer import Iteratorizer
from .serializer import Serializer
from .lrucache import lru_cache
from .diskusage import du
from .mustclose import must_close
from .urllib import urlencode

66
nilmdb/utils/lrucache.py Normal file
View File

@@ -0,0 +1,66 @@
# Memoize a function's return value with a least-recently-used cache
# Based on:
# http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/
# with added 'destructor' functionality.
import collections
import functools
def lru_cache(size = 10, onremove = None):
"""Least-recently-used cache decorator.
@lru_cache(size = 10, onevict = None)
def f(...):
pass
Given a function and arguments, memoize its return value.
Up to 'size' elements are cached.
When evicting a value from the cache, call the function
'onremove' with the value that's being evicted.
Call f.cache_remove(...) to evict the cache entry with the given
arguments. Call f.cache_remove_all() to evict all entries.
f.cache_hits and f.cache_misses give statistics.
"""
def decorator(func):
cache = collections.OrderedDict() # order: least- to most-recent
def evict(value):
if onremove:
onremove(value)
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = args + tuple(sorted(kwargs.items()))
try:
value = cache.pop(key)
wrapper.cache_hits += 1
except KeyError:
value = func(*args, **kwargs)
wrapper.cache_misses += 1
if len(cache) >= size:
evict(cache.popitem(0)[1]) # evict LRU cache entry
cache[key] = value # (re-)insert this key at end
return value
def cache_remove(*args, **kwargs):
"""Remove the described key from this cache, if present.
Note that if the original wrapped function was implicitly
passed 'self', you need to pass it as an argument here too."""
key = args + tuple(sorted(kwargs.items()))
if key in cache:
evict(cache.pop(key))
def cache_remove_all():
for key in cache:
evict(cache.pop(key))
wrapper.cache_hits = 0
wrapper.cache_misses = 0
wrapper.cache_remove = cache_remove
wrapper.cache_remove_all = cache_remove_all
return wrapper
return decorator

42
nilmdb/utils/mustclose.py Normal file
View File

@@ -0,0 +1,42 @@
# Class decorator that warns on stderr at deletion time if the class's
# close() member wasn't called.
from nilmdb.utils.printf import *
import sys
def must_close(errorfile = sys.stderr):
def decorator(cls):
def dummy(*args, **kwargs):
pass
if "__init__" not in cls.__dict__:
cls.__init__ = dummy
if "__del__" not in cls.__dict__:
cls.__del__ = dummy
if "close" not in cls.__dict__:
cls.close = dummy
orig_init = cls.__init__
orig_del = cls.__del__
orig_close = cls.close
def __init__(self, *args, **kwargs):
ret = orig_init(self, *args, **kwargs)
self.__dict__["_must_close"] = True
return ret
def __del__(self):
if "_must_close" in self.__dict__:
fprintf(errorfile, "error: %s.close() wasn't called!\n",
self.__class__.__name__)
return orig_del(self)
def close(self, *args, **kwargs):
del self._must_close
return orig_close(self)
cls.__init__ = __init__
cls.__del__ = __del__
cls.close = close
return cls
return decorator

View File

@@ -67,3 +67,6 @@ class WrapObject(object):
def __del__(self): def __del__(self):
self.__wrap_call_queue.put((None, None, None, None)) self.__wrap_call_queue.put((None, None, None, None))
self.__wrap_serializer.join() self.__wrap_serializer.join()
# Just an alias
Serializer = WrapObject

View File

@@ -5,6 +5,7 @@
# with nilmdb.Timer("flush"): # with nilmdb.Timer("flush"):
# foo.flush() # foo.flush()
from __future__ import print_function
import contextlib import contextlib
import time import time
@@ -18,4 +19,4 @@ def Timer(name = None, tosyslog = False):
import syslog import syslog
syslog.syslog(msg) syslog.syslog(msg)
else: else:
print msg print(msg)

68
nilmdb/utils/urllib.py Normal file
View File

@@ -0,0 +1,68 @@
from __future__ import absolute_import
from urllib import quote_plus, _is_unicode
# urllib.urlencode insists on encoding Unicode as ASCII. This is an
# exact copy of that function, except we encode it as UTF-8 instead.
def urlencode(query, doseq=0):
"""Encode a sequence of two-element tuples or dictionary into a URL query string.
If any values in the query arg are sequences and doseq is true, each
sequence element is converted to a separate parameter.
If the query arg is a sequence of two-element tuples, the order of the
parameters in the output will match the order of parameters in the
input.
"""
if hasattr(query,"items"):
# mapping objects
query = query.items()
else:
# it's a bother at times that strings and string-like objects are
# sequences...
try:
# non-sequence items should not work with len()
# non-empty strings will fail this
if len(query) and not isinstance(query[0], tuple):
raise TypeError
# zero-length sequences of all types will get here and succeed,
# but that's a minor nit - since the original implementation
# allowed empty dicts that type of behavior probably should be
# preserved for consistency
except TypeError:
ty,va,tb = sys.exc_info()
raise TypeError, "not a valid non-string sequence or mapping object", tb
l = []
if not doseq:
# preserve old behavior
for k, v in query:
k = quote_plus(str(k))
v = quote_plus(str(v))
l.append(k + '=' + v)
else:
for k, v in query:
k = quote_plus(str(k))
if isinstance(v, str):
v = quote_plus(v)
l.append(k + '=' + v)
elif _is_unicode(v):
# is there a reasonable way to convert to ASCII?
# encode generates a string, but "replace" or "ignore"
# lose information and "strict" can raise UnicodeError
v = quote_plus(v.encode("utf-8","strict"))
l.append(k + '=' + v)
else:
try:
# is this a sufficient test for sequence-ness?
len(v)
except TypeError:
# not a sequence
v = quote_plus(str(v))
l.append(k + '=' + v)
else:
# loop over the sequence
for elt in v:
l.append(k + '=' + quote_plus(str(elt)))
return '&'.join(l)

View File

@@ -3,14 +3,17 @@
import nilmdb import nilmdb
import argparse import argparse
parser = argparse.ArgumentParser(description='Run the NILM server') formatter = argparse.ArgumentDefaultsHelpFormatter
parser = argparse.ArgumentParser(description='Run the NILM server',
formatter_class = formatter)
parser.add_argument('-p', '--port', help='Port number', type=int, default=12380) parser.add_argument('-p', '--port', help='Port number', type=int, default=12380)
parser.add_argument('-d', '--database', help='Database directory', default="db")
parser.add_argument('-y', '--yappi', help='Run with yappi profiler', parser.add_argument('-y', '--yappi', help='Run with yappi profiler',
action='store_true') action='store_true')
args = parser.parse_args() args = parser.parse_args()
# Start web app on a custom port # Start web app on a custom port
db = nilmdb.NilmDB("db") db = nilmdb.NilmDB(args.database)
server = nilmdb.Server(db, host = "127.0.0.1", server = nilmdb.Server(db, host = "127.0.0.1",
port = args.port, port = args.port,
embedded = False) embedded = False)

View File

@@ -10,10 +10,14 @@ cover-erase=
##cover-branches= # need nose 1.1.3 for this ##cover-branches= # need nose 1.1.3 for this
stop= stop=
verbosity=2 verbosity=2
#tests=tests/test_mustclose.py
#tests=tests/test_lrucache.py
#tests=tests/test_cmdline.py #tests=tests/test_cmdline.py
#tests=tests/test_layout.py #tests=tests/test_layout.py
#tests=tests/test_rbtree.py #tests=tests/test_rbtree.py
tests=tests/test_interval.py #tests=tests/test_interval.py
#tests=tests/test_rbtree.py,tests/test_interval.py
#tests=tests/test_interval.py
#tests=tests/test_client.py #tests=tests/test_client.py
#tests=tests/test_timestamper.py #tests=tests/test_timestamper.py
#tests=tests/test_serializer.py #tests=tests/test_serializer.py

90
tests/renderdot.py Normal file
View File

@@ -0,0 +1,90 @@
import sys
class Renderer(object):
def __init__(self, getleft, getright,
getred, getstart, getend, nil):
self.getleft = getleft
self.getright = getright
self.getred = getred
self.getstart = getstart
self.getend = getend
self.nil = nil
# Rendering
def __render_dot_node(self, node, max_depth = 20):
from nilmdb.utils.printf import sprintf
"""Render a single node and its children into a dot graph fragment"""
if max_depth == 0:
return ""
if node is self.nil:
return ""
def c(red):
if red:
return 'color="#ff0000", style=filled, fillcolor="#ffc0c0"'
else:
return 'color="#000000", style=filled, fillcolor="#c0c0c0"'
s = sprintf("%d [label=\"%g\\n%g\", %s];\n",
id(node),
self.getstart(node), self.getend(node),
c(self.getred(node)))
if self.getleft(node) is self.nil:
s += sprintf("L%d [label=\"-\", %s];\n", id(node), c(False))
s += sprintf("%d -> L%d [label=L];\n", id(node), id(node))
else:
s += sprintf("%d -> %d [label=L];\n",
id(node),id(self.getleft(node)))
if self.getright(node) is self.nil:
s += sprintf("R%d [label=\"-\", %s];\n", id(node), c(False))
s += sprintf("%d -> R%d [label=R];\n", id(node), id(node))
else:
s += sprintf("%d -> %d [label=R];\n",
id(node), id(self.getright(node)))
s += self.__render_dot_node(self.getleft(node), max_depth-1)
s += self.__render_dot_node(self.getright(node), max_depth-1)
return s
def render_dot(self, rootnode, title = "Tree"):
"""Render the entire tree as a dot graph"""
return ("digraph rbtree {\n"
+ self.__render_dot_node(rootnode)
+ "}\n");
def render_dot_live(self, rootnode, title = "Tree"):
"""Render the entiretree as a dot graph, live GTK view"""
import gtk
import gtk.gdk
sys.path.append("/usr/share/xdot")
import xdot
xdot.Pen.highlighted = lambda pen: pen
s = ("digraph rbtree {\n"
+ self.__render_dot_node(rootnode)
+ "}\n");
window = xdot.DotWindow()
window.set_dotcode(s)
window.set_title(title + " - any key to close")
window.connect('destroy', gtk.main_quit)
def quit(widget, event):
if not event.is_modifier:
window.destroy()
gtk.main_quit()
window.widget.connect('key-press-event', quit)
gtk.main()
class RBTreeRenderer(Renderer):
def __init__(self, tree):
Renderer.__init__(self,
lambda node: node.left,
lambda node: node.right,
lambda node: node.red,
lambda node: node.start,
lambda node: node.end,
tree.nil)
self.tree = tree
def render(self, title = "RBTree", live = True):
if live:
return Renderer.render_dot_live(self, self.tree.getroot(), title)
else:
return Renderer.render_dot(self, self.tree.getroot(), title)

View File

@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
from nilmdb.client import ClientError, ServerError from nilmdb.client import ClientError, ServerError
import datetime_tz import datetime_tz
@@ -82,6 +84,8 @@ class TestClient(object):
# Bad layout type # Bad layout type
with assert_raises(ClientError): with assert_raises(ClientError):
client.stream_create("/newton/prep", "NoSuchLayout") client.stream_create("/newton/prep", "NoSuchLayout")
# Create three streams
client.stream_create("/newton/prep", "PrepData") client.stream_create("/newton/prep", "PrepData")
client.stream_create("/newton/raw", "RawData") client.stream_create("/newton/raw", "RawData")
client.stream_create("/newton/zzz/rawnotch", "RawNotchedData") client.stream_create("/newton/zzz/rawnotch", "RawNotchedData")
@@ -131,6 +135,7 @@ class TestClient(object):
testfile = "tests/data/prep-20120323T1000" testfile = "tests/data/prep-20120323T1000"
start = datetime_tz.datetime_tz.smartparse("20120323T1000") start = datetime_tz.datetime_tz.smartparse("20120323T1000")
start = start.totimestamp()
rate = 120 rate = 120
# First try a nonexistent path # First try a nonexistent path
@@ -155,14 +160,41 @@ class TestClient(object):
# Try forcing a server request with empty data # Try forcing a server request with empty data
with assert_raises(ClientError) as e: with assert_raises(ClientError) as e:
client.http.put("stream/insert", "", { "path": "/newton/prep" }) client.http.put("stream/insert", "", { "path": "/newton/prep",
"start": 0, "end": 0 })
in_("400 Bad Request", str(e.exception)) in_("400 Bad Request", str(e.exception))
in_("no data provided", str(e.exception)) in_("no data provided", str(e.exception))
# Specify start/end (starts too late)
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
with assert_raises(ClientError) as e:
result = client.stream_insert("/newton/prep", data,
start + 5, start + 120)
in_("400 Bad Request", str(e.exception))
in_("Data timestamp 1332511200.0 < start time 1332511205.0",
str(e.exception))
# Specify start/end (ends too early)
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
with assert_raises(ClientError) as e:
result = client.stream_insert("/newton/prep", data,
start, start + 1)
in_("400 Bad Request", str(e.exception))
# Client chunks the input, so the exact timestamp here might change
# if the chunk positions change.
in_("Data timestamp 1332511271.016667 >= end time 1332511201.0",
str(e.exception))
# Now do the real load # Now do the real load
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120) data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
result = client.stream_insert("/newton/prep", data) result = client.stream_insert("/newton/prep", data,
eq_(result[0], "ok") start, start + 119.999777)
eq_(result, "ok")
# Verify the intervals. Should be just one, even if the data
# was inserted in chunks, due to nilmdb interval concatenation.
intervals = list(client.stream_intervals("/newton/prep"))
eq_(intervals, [[start, start + 119.999777]])
# Try some overlapping data -- just insert it again # Try some overlapping data -- just insert it again
data = nilmdb.timestamper.TimestamperRate(testfile, start, 120) data = nilmdb.timestamper.TimestamperRate(testfile, start, 120)
@@ -215,7 +247,8 @@ class TestClient(object):
# Check PUT with generator out # Check PUT with generator out
with assert_raises(ClientError) as e: with assert_raises(ClientError) as e:
client.http.put_gen("stream/insert", "", client.http.put_gen("stream/insert", "",
{ "path": "/newton/prep" }).next() { "path": "/newton/prep",
"start": 0, "end": 0 }).next()
in_("400 Bad Request", str(e.exception)) in_("400 Bad Request", str(e.exception))
in_("no data provided", str(e.exception)) in_("no data provided", str(e.exception))
@@ -238,7 +271,7 @@ class TestClient(object):
# still disable chunked responses for debugging. # still disable chunked responses for debugging.
x = client.http.get("stream/intervals", { "path": "/newton/prep" }, x = client.http.get("stream/intervals", { "path": "/newton/prep" },
retjson=False) retjson=False)
eq_(x.count('\n'), 2) lines_(x, 1)
if "transfer-encoding: chunked" not in client.http._headers.lower(): if "transfer-encoding: chunked" not in client.http._headers.lower():
warnings.warn("Non-chunked HTTP response for /stream/intervals") warnings.warn("Non-chunked HTTP response for /stream/intervals")
@@ -248,3 +281,40 @@ class TestClient(object):
"end": "123" }, retjson=False) "end": "123" }, retjson=False)
if "transfer-encoding: chunked" not in client.http._headers.lower(): if "transfer-encoding: chunked" not in client.http._headers.lower():
warnings.warn("Non-chunked HTTP response for /stream/extract") warnings.warn("Non-chunked HTTP response for /stream/extract")
def test_client_7_unicode(self):
# Basic Unicode tests
client = nilmdb.Client(url = "http://localhost:12380/")
# Delete streams that exist
for stream in client.stream_list():
client.stream_destroy(stream[0])
# Database is empty
eq_(client.stream_list(), [])
# Create Unicode stream, match it
raw = [ u"/düsseldorf/raw", u"uint16_6" ]
prep = [ u"/düsseldorf/prep", u"uint16_6" ]
client.stream_create(*raw)
eq_(client.stream_list(), [raw])
eq_(client.stream_list(layout=raw[1]), [raw])
eq_(client.stream_list(path=raw[0]), [raw])
client.stream_create(*prep)
eq_(client.stream_list(), [prep, raw])
# Set / get metadata with Unicode keys and values
eq_(client.stream_get_metadata(raw[0]), {})
eq_(client.stream_get_metadata(prep[0]), {})
meta1 = { u"alpha": u"α",
u"β": u"beta" }
meta2 = { u"alpha": u"α" }
meta3 = { u"β": u"beta" }
client.stream_set_metadata(prep[0], meta1)
client.stream_update_metadata(prep[0], {})
client.stream_update_metadata(raw[0], meta2)
client.stream_update_metadata(raw[0], meta3)
eq_(client.stream_get_metadata(prep[0]), meta1)
eq_(client.stream_get_metadata(raw[0]), meta1)
eq_(client.stream_get_metadata(raw[0], [ "alpha" ]), meta2)
eq_(client.stream_get_metadata(raw[0], [ "alpha", "β" ]), meta1)

View File

@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import nilmdb.cmdline import nilmdb.cmdline
from nose.tools import * from nose.tools import *
@@ -13,7 +15,7 @@ import threading
import urllib2 import urllib2
from urllib2 import urlopen, HTTPError from urllib2 import urlopen, HTTPError
import Queue import Queue
import cStringIO import StringIO
import shlex import shlex
from test_helpers import * from test_helpers import *
@@ -45,12 +47,18 @@ def setup_module():
def teardown_module(): def teardown_module():
server_stop() server_stop()
# Add an encoding property to StringIO so Python will convert Unicode
# properly when writing or reading.
class UTF8StringIO(StringIO.StringIO):
encoding = 'utf-8'
class TestCmdline(object): class TestCmdline(object):
def run(self, arg_string, infile=None, outfile=None): def run(self, arg_string, infile=None, outfile=None):
"""Run a cmdline client with the specified argument string, """Run a cmdline client with the specified argument string,
passing the given input. Returns a tuple with the output and passing the given input. Returns a tuple with the output and
exit code""" exit code"""
# printf("TZ=UTC ./nilmtool.py %s\n", arg_string)
class stdio_wrapper: class stdio_wrapper:
def __init__(self, stdin, stdout, stderr): def __init__(self, stdin, stdout, stderr):
self.io = (stdin, stdout, stderr) self.io = (stdin, stdout, stderr)
@@ -61,15 +69,18 @@ class TestCmdline(object):
( sys.stdin, sys.stdout, sys.stderr ) = self.saved ( sys.stdin, sys.stdout, sys.stderr ) = self.saved
# Empty input if none provided # Empty input if none provided
if infile is None: if infile is None:
infile = cStringIO.StringIO("") infile = UTF8StringIO("")
# Capture stderr # Capture stderr
errfile = cStringIO.StringIO() errfile = UTF8StringIO()
if outfile is None: if outfile is None:
# If no output file, capture stdout with stderr # If no output file, capture stdout with stderr
outfile = errfile outfile = errfile
with stdio_wrapper(infile, outfile, errfile) as s: with stdio_wrapper(infile, outfile, errfile) as s:
try: try:
nilmdb.cmdline.Cmdline(shlex.split(arg_string)).run() # shlex doesn't support Unicode very well. Encode the
# string as UTF-8 explicitly before splitting.
args = shlex.split(arg_string.encode('utf-8'))
nilmdb.cmdline.Cmdline(args).run()
sys.exit(0) sys.exit(0)
except SystemExit as e: except SystemExit as e:
exitcode = e.code exitcode = e.code
@@ -192,11 +203,22 @@ class TestCmdline(object):
self.contain("no such layout") self.contain("no such layout")
# Create a few streams # Create a few streams
self.ok("create /newton/zzz/rawnotch RawNotchedData")
self.ok("create /newton/prep PrepData") self.ok("create /newton/prep PrepData")
self.ok("create /newton/raw RawData") self.ok("create /newton/raw RawData")
self.ok("create /newton/zzz/rawnotch RawNotchedData")
# Verify we got those 3 streams # Should not be able to create a stream with another stream as
# its parent
self.fail("create /newton/prep/blah PrepData")
self.contain("path is subdir of existing node")
# Should not be able to create a stream at a location that
# has other nodes as children
self.fail("create /newton/zzz PrepData")
self.contain("subdirs of this path already exist")
# Verify we got those 3 streams and they're returned in
# alphabetical order.
self.ok("list") self.ok("list")
self.match("/newton/prep PrepData\n" self.match("/newton/prep PrepData\n"
"/newton/raw RawData\n" "/newton/raw RawData\n"
@@ -286,16 +308,9 @@ class TestCmdline(object):
eq_(cmd.parse_time("hi there 20120405 1400-0400 testing! 123"), test) eq_(cmd.parse_time("hi there 20120405 1400-0400 testing! 123"), test)
eq_(cmd.parse_time("20120405 1800 UTC"), test) eq_(cmd.parse_time("20120405 1800 UTC"), test)
eq_(cmd.parse_time("20120405 1400-0400 UTC"), test) eq_(cmd.parse_time("20120405 1400-0400 UTC"), test)
with assert_raises(ValueError): for badtime in [ "20120405 1400-9999", "hello", "-", "", "14:00" ]:
print cmd.parse_time("20120405 1400-9999") with assert_raises(ValueError):
with assert_raises(ValueError): x = cmd.parse_time(badtime)
print cmd.parse_time("hello")
with assert_raises(ValueError):
print cmd.parse_time("-")
with assert_raises(ValueError):
print cmd.parse_time("")
with assert_raises(ValueError):
print cmd.parse_time("14:00")
eq_(cmd.parse_time("snapshot-20120405-140000.raw.gz"), test) eq_(cmd.parse_time("snapshot-20120405-140000.raw.gz"), test)
eq_(cmd.parse_time("prep-20120405T1400"), test) eq_(cmd.parse_time("prep-20120405T1400"), test)
@@ -362,36 +377,36 @@ class TestCmdline(object):
def test_cmdline_07_detail(self): def test_cmdline_07_detail(self):
# Just count the number of lines, it's probably fine # Just count the number of lines, it's probably fine
self.ok("list --detail") self.ok("list --detail")
eq_(self.captured.count('\n'), 11) lines_(self.captured, 8)
self.ok("list --detail --path *prep") self.ok("list --detail --path *prep")
eq_(self.captured.count('\n'), 7) lines_(self.captured, 4)
self.ok("list --detail --path *prep --start='23 Mar 2012 10:02'") self.ok("list --detail --path *prep --start='23 Mar 2012 10:02'")
eq_(self.captured.count('\n'), 5) lines_(self.captured, 3)
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05'") self.ok("list --detail --path *prep --start='23 Mar 2012 10:05'")
eq_(self.captured.count('\n'), 3) lines_(self.captured, 2)
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15'") self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15'")
eq_(self.captured.count('\n'), 2) lines_(self.captured, 2)
self.contain("10:05:15.000") self.contain("10:05:15.000")
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'") self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'")
eq_(self.captured.count('\n'), 2) lines_(self.captured, 2)
self.contain("10:05:15.500") self.contain("10:05:15.500")
self.ok("list --detail --path *prep --start='23 Mar 2012 19:05:15.50'") self.ok("list --detail --path *prep --start='23 Mar 2012 19:05:15.50'")
eq_(self.captured.count('\n'), 2) lines_(self.captured, 2)
self.contain("no intervals") self.contain("no intervals")
self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'" self.ok("list --detail --path *prep --start='23 Mar 2012 10:05:15.50'"
+ " --end='23 Mar 2012 10:05:15.50'") + " --end='23 Mar 2012 10:05:15.50'")
eq_(self.captured.count('\n'), 2) lines_(self.captured, 2)
self.contain("10:05:15.500") self.contain("10:05:15.500")
self.ok("list --detail") self.ok("list --detail")
eq_(self.captured.count('\n'), 11) lines_(self.captured, 8)
def test_cmdline_08_extract(self): def test_cmdline_08_extract(self):
# nonexistent stream # nonexistent stream
@@ -444,7 +459,7 @@ class TestCmdline(object):
# all data put in by tests # all data put in by tests
self.ok("extract -a /newton/prep --start 2000-01-01 --end 2020-01-01") self.ok("extract -a /newton/prep --start 2000-01-01 --end 2020-01-01")
eq_(self.captured.count('\n'), 43204) lines_(self.captured, 43204)
self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01") self.ok("extract -c /newton/prep --start 2000-01-01 --end 2020-01-01")
self.match("43200\n") self.match("43200\n")
@@ -453,6 +468,75 @@ class TestCmdline(object):
server_stop() server_stop()
server_start(max_results = 2) server_start(max_results = 2)
self.ok("list --detail") self.ok("list --detail")
eq_(self.captured.count('\n'), 11) lines_(self.captured, 8)
server_stop() server_stop()
server_start() server_start()
def test_cmdline_10_destroy(self):
# Delete records
self.ok("destroy --help")
self.fail("destroy")
self.contain("too few arguments")
self.fail("destroy /no/such/stream")
self.contain("No stream at path")
self.fail("destroy asdfasdf")
self.contain("No stream at path")
# From previous tests, we have:
self.ok("list")
self.match("/newton/prep PrepData\n"
"/newton/raw RawData\n"
"/newton/zzz/rawnotch RawNotchedData\n")
# Notice how they're not empty
self.ok("list --detail")
lines_(self.captured, 8)
# Delete some
self.ok("destroy /newton/prep")
self.ok("list")
self.match("/newton/raw RawData\n"
"/newton/zzz/rawnotch RawNotchedData\n")
self.ok("destroy /newton/zzz/rawnotch")
self.ok("list")
self.match("/newton/raw RawData\n")
self.ok("destroy /newton/raw")
self.ok("create /newton/raw RawData")
self.ok("destroy /newton/raw")
self.ok("list")
self.match("")
# Re-create a previously deleted location, and some new ones
rebuild = [ "/newton/prep", "/newton/zzz",
"/newton/raw", "/newton/asdf/qwer" ]
for path in rebuild:
# Create the path
self.ok("create " + path + " PrepData")
self.ok("list")
self.contain(path)
# Make sure it was created empty
self.ok("list --detail --path " + path)
self.contain("(no intervals)")
def test_cmdline_11_unicode(self):
# Unicode paths.
self.ok("destroy /newton/asdf/qwer")
self.ok("destroy /newton/prep")
self.ok("destroy /newton/raw")
self.ok("destroy /newton/zzz")
self.ok(u"create /düsseldorf/raw uint16_6")
self.ok("list --detail")
self.contain(u"/düsseldorf/raw uint16_6")
self.contain("(no intervals)")
# Unicode metadata
self.ok(u"metadata /düsseldorf/raw --set α=beta 'γ'")
self.ok(u"metadata /düsseldorf/raw --update 'α=β ε τ α'")
self.ok(u"metadata /düsseldorf/raw")
self.match(u"α=β ε τ α\nγ\n")

View File

@@ -20,6 +20,12 @@ def ne_(a, b):
if not a != b: if not a != b:
raise AssertionError("unexpected %s == %s" % (myrepr(a), myrepr(b))) raise AssertionError("unexpected %s == %s" % (myrepr(a), myrepr(b)))
def lines_(a, n):
l = a.count('\n')
if not l == n:
raise AssertionError("wanted %d lines, got %d in output: '%s'"
% (n, l, a))
def recursive_unlink(path): def recursive_unlink(path):
try: try:
shutil.rmtree(path) shutil.rmtree(path)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import datetime_tz import datetime_tz
from nose.tools import * from nose.tools import *
@@ -13,12 +13,19 @@ from nilmdb.interval import Interval, DBInterval, IntervalSet, IntervalError
from test_helpers import * from test_helpers import *
import unittest import unittest
# set to False to skip live renders
do_live_renders = False
def render(iset, description = "", live = True):
import renderdot
r = renderdot.RBTreeRenderer(iset.tree)
return r.render(description, live and do_live_renders)
def makeset(string): def makeset(string):
"""Build an IntervalSet from a string, for testing purposes """Build an IntervalSet from a string, for testing purposes
Each character is 1 second Each character is 1 second
[ = interval start [ = interval start
| = interval end + adjacent start | = interval end + next start
] = interval end ] = interval end
. = zero-width interval (identical start and end) . = zero-width interval (identical start and end)
anything else is ignored anything else is ignored
@@ -31,7 +38,7 @@ def makeset(string):
elif (c == "|"): elif (c == "|"):
iset += Interval(start, day) iset += Interval(start, day)
start = day start = day
elif (c == "]"): elif (c == ")"):
iset += Interval(start, day) iset += Interval(start, day)
del start del start
elif (c == "."): elif (c == "."):
@@ -71,24 +78,24 @@ class TestInterval:
assert(Interval(d1, d3) < Interval(d2, d3)) assert(Interval(d1, d3) < Interval(d2, d3))
assert(Interval(d2, d2) > Interval(d1, d3)) assert(Interval(d2, d2) > Interval(d1, d3))
assert(Interval(d3, d3) == Interval(d3, d3)) assert(Interval(d3, d3) == Interval(d3, d3))
with assert_raises(TypeError): # was AttributeError, that's wrong #with assert_raises(TypeError): # was AttributeError, that's wrong
x = (i == 123) # x = (i == 123)
# subset # subset
assert(Interval(d1, d3).subset(d1, d2) == Interval(d1, d2)) eq_(Interval(d1, d3).subset(d1, d2), Interval(d1, d2))
with assert_raises(IntervalError): with assert_raises(IntervalError):
x = Interval(d2, d3).subset(d1, d2) x = Interval(d2, d3).subset(d1, d2)
# big integers and floats # big integers and floats
x = Interval(5000111222, 6000111222) x = Interval(5000111222, 6000111222)
eq_(str(x), "[5000111222.0 -> 6000111222.0]") eq_(str(x), "[5000111222.0 -> 6000111222.0)")
x = Interval(123.45, 234.56) x = Interval(123.45, 234.56)
eq_(str(x), "[123.45 -> 234.56]") eq_(str(x), "[123.45 -> 234.56)")
# misc # misc
i = Interval(d1, d2) i = Interval(d1, d2)
eq_(repr(i), repr(eval(repr(i)))) eq_(repr(i), repr(eval(repr(i))))
eq_(str(i), "[1332561600.0 -> 1332648000.0]") eq_(str(i), "[1332561600.0 -> 1332648000.0)")
def test_interval_intersect(self): def test_interval_intersect(self):
# Test Interval intersections # Test Interval intersections
@@ -109,7 +116,7 @@ class TestInterval:
except IntervalError: except IntervalError:
assert(i not in should_intersect[True] and assert(i not in should_intersect[True] and
i not in should_intersect[False]) i not in should_intersect[False])
with assert_raises(AttributeError): with assert_raises(TypeError):
x = i1.intersects(1234) x = i1.intersects(1234)
def test_intervalset_construct(self): def test_intervalset_construct(self):
@@ -130,6 +137,15 @@ class TestInterval:
x = iseta != 3 x = iseta != 3
ne_(IntervalSet(a), IntervalSet(b)) ne_(IntervalSet(a), IntervalSet(b))
# Note that assignment makes a new reference (not a copy)
isetd = IntervalSet(isetb)
isete = isetd
eq_(isetd, isetb)
eq_(isetd, isete)
isetd -= a
ne_(isetd, isetb)
eq_(isetd, isete)
# test iterator # test iterator
for interval in iseta: for interval in iseta:
pass pass
@@ -151,11 +167,18 @@ class TestInterval:
iset = IntervalSet(a) iset = IntervalSet(a)
iset += IntervalSet(b) iset += IntervalSet(b)
eq_(iset, IntervalSet([a, b])) eq_(iset, IntervalSet([a, b]))
iset = IntervalSet(a) iset = IntervalSet(a)
iset += b iset += b
eq_(iset, IntervalSet([a, b])) eq_(iset, IntervalSet([a, b]))
iset = IntervalSet(a)
iset.iadd_nocheck(b)
eq_(iset, IntervalSet([a, b]))
iset = IntervalSet(a) + IntervalSet(b) iset = IntervalSet(a) + IntervalSet(b)
eq_(iset, IntervalSet([a, b])) eq_(iset, IntervalSet([a, b]))
iset = IntervalSet(b) + a iset = IntervalSet(b) + a
eq_(iset, IntervalSet([a, b])) eq_(iset, IntervalSet([a, b]))
@@ -168,61 +191,79 @@ class TestInterval:
# misc # misc
eq_(repr(iset), repr(eval(repr(iset)))) eq_(repr(iset), repr(eval(repr(iset))))
eq_(str(iset), "[[100.0 -> 200.0], [200.0 -> 300.0]]") eq_(str(iset), "[[100.0 -> 200.0), [200.0 -> 300.0)]")
def test_intervalset_geniset(self): def test_intervalset_geniset(self):
# Test basic iset construction # Test basic iset construction
assert(makeset(" [----] ") == eq_(makeset(" [----) "),
makeset(" [-|--] ")) makeset(" [-|--) "))
assert(makeset("[] [--] ") + eq_(makeset("[) [--) ") +
makeset(" [] [--]") == makeset(" [) [--)"),
makeset("[|] [-----]")) makeset("[|) [-----)"))
assert(makeset(" [-------]") == eq_(makeset(" [-------)"),
makeset(" [-|-----|")) makeset(" [-|-----|"))
def test_intervalset_intersect(self): def test_intervalset_intersect(self):
# Test intersection (&) # Test intersection (&)
with assert_raises(TypeError): # was AttributeError with assert_raises(TypeError): # was AttributeError
x = makeset("[--]") & 1234 x = makeset("[--)") & 1234
assert(makeset("[---------]") & # Intersection with interval
makeset(" [---] ") == eq_(makeset("[---|---)[)") &
makeset(" [---] ")) list(makeset(" [------) "))[0],
makeset(" [-----) "))
assert(makeset(" [---] ") & # Intersection with sets
makeset("[---------]") == eq_(makeset("[---------)") &
makeset(" [---] ")) makeset(" [---) "),
makeset(" [---) "))
assert(makeset(" [-----]") & eq_(makeset(" [---) ") &
makeset(" [-----] ") == makeset("[---------)"),
makeset(" [--] ")) makeset(" [---) "))
assert(makeset(" [--] [--]") & eq_(makeset(" [-----)") &
makeset(" [------] ") == makeset(" [-----) "),
makeset(" [-] [-] ")) makeset(" [--) "))
assert(makeset(" [---]") & eq_(makeset(" [--) [--)") &
makeset(" [--] ") == makeset(" [------) "),
makeset(" ")) makeset(" [-) [-) "))
assert(makeset(" [---]") & eq_(makeset(" [---)") &
makeset(" [----] ") == makeset(" [--) "),
makeset(" . ")) makeset(" "))
assert(makeset(" [-|---]") & eq_(makeset(" [-|---)") &
makeset(" [-----|-] ") == makeset(" [-----|-) "),
makeset(" [----] ")) makeset(" [----) "))
assert(makeset(" [-|-] ") & eq_(makeset(" [-|-) ") &
makeset(" [-|--|--] ") == makeset(" [-|--|--) "),
makeset(" [---] ")) makeset(" [---) "))
assert(makeset(" [----][--]") & # Border cases -- will give different results if intervals are
makeset("[-] [--] []") == # half open or fully closed. Right now, they are half open,
makeset(" [] [-]. []")) # although that's a little messy since the database intervals
# often contain a data point at the endpoint.
half_open = True
if half_open:
eq_(makeset(" [---)") &
makeset(" [----) "),
makeset(" "))
eq_(makeset(" [----)[--)") &
makeset("[-) [--) [)"),
makeset(" [) [-) [)"))
else:
eq_(makeset(" [---)") &
makeset(" [----) "),
makeset(" . "))
eq_(makeset(" [----)[--)") &
makeset("[-) [--) [)"),
makeset(" [) [-). [)"))
class TestIntervalDB: class TestIntervalDB:
def test_dbinterval(self): def test_dbinterval(self):
@@ -273,12 +314,13 @@ class TestIntervalTree:
import random import random
random.seed(1234) random.seed(1234)
# make a set of 500 intervals # make a set of 100 intervals
iset = IntervalSet() iset = IntervalSet()
j = 500 j = 100
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
interval = Interval(i, i+1) interval = Interval(i, i+1)
iset += interval iset += interval
render(iset, "Random Insertion")
# remove about half of them # remove about half of them
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
@@ -288,10 +330,15 @@ class TestIntervalTree:
# try removing an interval that doesn't exist # try removing an interval that doesn't exist
with assert_raises(IntervalError): with assert_raises(IntervalError):
iset -= Interval(1234,5678) iset -= Interval(1234,5678)
render(iset, "Random Insertion, deletion")
# show the graph # make a set of 100 intervals, inserted in order
if False: iset = IntervalSet()
iset.tree.render_dot_live() j = 100
for i in xrange(j):
interval = Interval(i, i+1)
iset += interval
render(iset, "In-order insertion")
class TestIntervalSpeed: class TestIntervalSpeed:
@unittest.skip("this is slow") @unittest.skip("this is slow")
@@ -300,18 +347,23 @@ class TestIntervalSpeed:
import time import time
import aplotter import aplotter
import random import random
import math
print print
yappi.start() yappi.start()
speeds = {} speeds = {}
for j in [ 2**x for x in range(5,18) ]: for j in [ 2**x for x in range(5,20) ]:
start = time.time() start = time.time()
iset = IntervalSet() iset = IntervalSet()
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
interval = Interval(i, i+1) interval = Interval(i, i+1)
iset += interval iset += interval
speed = (time.time() - start) * 1000000.0 speed = (time.time() - start) * 1000000.0
printf("%d: %g μs (%g μs each)\n", j, speed, speed/j) printf("%d: %g μs (%g μs each, O(n log n) ratio %g)\n",
j,
speed,
speed/j,
speed / (j*math.log(j))) # should be constant
speeds[j] = speed speeds[j] = speed
aplotter.plot(speeds.keys(), speeds.values(), plot_slope=True) aplotter.plot(speeds.keys(), speeds.values(), plot_slope=True)
yappi.stop() yappi.stop()

View File

@@ -1,5 +1,5 @@
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import nose import nose
from nose.tools import * from nose.tools import *
@@ -9,8 +9,6 @@ import time
from test_helpers import * from test_helpers import *
import nilmdb.iteratorizer
def func_with_callback(a, b, callback): def func_with_callback(a, b, callback):
callback(a) callback(a)
callback(b) callback(b)
@@ -27,16 +25,18 @@ class TestIteratorizer(object):
eq_(self.result, "123") eq_(self.result, "123")
# Now make it an iterator # Now make it an iterator
it = nilmdb.iteratorizer.Iteratorizer(lambda x: it = nilmdb.utils.Iteratorizer(
func_with_callback(1, 2, x)) lambda x:
func_with_callback(1, 2, x))
result = "" result = ""
for i in it: for i in it:
result += str(i) result += str(i)
eq_(result, "123") eq_(result, "123")
# Make sure things work when an exception occurs # Make sure things work when an exception occurs
it = nilmdb.iteratorizer.Iteratorizer(lambda x: it = nilmdb.utils.Iteratorizer(
func_with_callback(1, "a", x)) lambda x:
func_with_callback(1, "a", x))
result = "" result = ""
with assert_raises(TypeError) as e: with assert_raises(TypeError) as e:
for i in it: for i in it:
@@ -48,7 +48,8 @@ class TestIteratorizer(object):
# itself. This doesn't have a particular result in the test, # itself. This doesn't have a particular result in the test,
# but gains coverage. # but gains coverage.
def foo(): def foo():
it = nilmdb.iteratorizer.Iteratorizer(lambda x: it = nilmdb.utils.Iteratorizer(
func_with_callback(1, 2, x)) lambda x:
func_with_callback(1, 2, x))
it.next() it.next()
foo() foo()

View File

@@ -2,7 +2,7 @@
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
from nose.tools import * from nose.tools import *
from nose.tools import assert_raises from nose.tools import assert_raises
@@ -28,9 +28,13 @@ class TestLayouts(object):
# Some nilmdb.layout tests. Not complete, just fills in missing # Some nilmdb.layout tests. Not complete, just fills in missing
# coverage. # coverage.
def test_layouts(self): def test_layouts(self):
x = nilmdb.layout.get_named("PrepData").description() x = nilmdb.layout.get_named("PrepData")
y = nilmdb.layout.get_named("float32_8").description() y = nilmdb.layout.get_named("float32_8")
eq_(repr(x), repr(y)) eq_(x.count, y.count)
eq_(x.datatype, y.datatype)
y = nilmdb.layout.get_named("float32_7")
ne_(x.count, y.count)
eq_(x.datatype, y.datatype)
def test_parsing(self): def test_parsing(self):
self.real_t_parsing("PrepData", "RawData", "RawNotchedData") self.real_t_parsing("PrepData", "RawData", "RawNotchedData")

53
tests/test_lrucache.py Normal file
View File

@@ -0,0 +1,53 @@
import nilmdb
from nilmdb.utils.printf import *
import nose
from nose.tools import *
from nose.tools import assert_raises
import threading
import time
from test_helpers import *
@nilmdb.utils.lru_cache(size = 3)
def foo1(n):
return n
@nilmdb.utils.lru_cache(size = 5)
def foo2(n):
return n
def foo3d(n):
foo3d.destructed.append(n)
foo3d.destructed = []
@nilmdb.utils.lru_cache(size = 3, onremove = foo3d)
def foo3(n):
return n
class TestLRUCache(object):
def test(self):
[ foo1(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo1.cache_hits, foo1.cache_misses), (6, 3))
[ foo1(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo1.cache_hits, foo1.cache_misses), (15, 3))
[ foo1(n) for n in [ 4, 2, 1, 1, 4 ] ]
eq_((foo1.cache_hits, foo1.cache_misses), (18, 5))
[ foo2(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo2.cache_hits, foo2.cache_misses), (6, 3))
[ foo2(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo2.cache_hits, foo2.cache_misses), (15, 3))
[ foo2(n) for n in [ 4, 2, 1, 1, 4 ] ]
eq_((foo2.cache_hits, foo2.cache_misses), (19, 4))
[ foo3(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo3.cache_hits, foo3.cache_misses), (6, 3))
[ foo3(n) for n in [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ] ]
eq_((foo3.cache_hits, foo3.cache_misses), (15, 3))
[ foo3(n) for n in [ 4, 2, 1, 1, 4 ] ]
eq_((foo3.cache_hits, foo3.cache_misses), (18, 5))
eq_(foo3d.destructed, [1, 3])
foo3.cache_remove(1)
eq_(foo3d.destructed, [1, 3, 1])
foo3.cache_remove_all()
eq_(foo3d.destructed, [1, 3, 1, 2, 4 ])

59
tests/test_mustclose.py Normal file
View File

@@ -0,0 +1,59 @@
import nilmdb
from nilmdb.utils.printf import *
import nose
from nose.tools import *
from nose.tools import assert_raises
from test_helpers import *
import sys
import cStringIO
err = cStringIO.StringIO()
@nilmdb.utils.must_close(errorfile = err)
class Foo:
def __init__(self):
fprintf(err, "Init\n")
def __del__(self):
fprintf(err, "Deleting\n")
def close(self):
fprintf(err, "Closing\n")
@nilmdb.utils.must_close(errorfile = err)
class Bar:
pass
class TestMustClose(object):
def test(self):
# Note: this test might fail if the Python interpreter doesn't
# garbage collect the object (and call its __del__ function)
# right after a "del x".
x = Foo()
del x
eq_(err.getvalue(),
"Init\n"
"error: Foo.close() wasn't called!\n"
"Deleting\n")
err.truncate(0)
y = Foo()
y.close()
del y
eq_(err.getvalue(),
"Init\n"
"Closing\n"
"Deleting\n")
err.truncate(0)
z = Bar()
z.close()
del z
eq_(err.getvalue(), "")

View File

@@ -14,6 +14,7 @@ import urllib2
from urllib2 import urlopen, HTTPError from urllib2 import urlopen, HTTPError
import Queue import Queue
import cStringIO import cStringIO
import time
testdb = "tests/testdb" testdb = "tests/testdb"
@@ -39,8 +40,8 @@ class Test00Nilmdb(object): # named 00 so it runs first
capture = cStringIO.StringIO() capture = cStringIO.StringIO()
old = sys.stdout old = sys.stdout
sys.stdout = capture sys.stdout = capture
with nilmdb.Timer("test"): with nilmdb.utils.Timer("test"):
nilmdb.timer.time.sleep(0.01) time.sleep(0.01)
sys.stdout = old sys.stdout = old
in_("test: ", capture.getvalue()) in_("test: ", capture.getvalue())
@@ -69,12 +70,14 @@ class Test00Nilmdb(object): # named 00 so it runs first
eq_(db.stream_list(layout="RawData"), [ ["/newton/raw", "RawData"] ]) eq_(db.stream_list(layout="RawData"), [ ["/newton/raw", "RawData"] ])
eq_(db.stream_list(path="/newton/raw"), [ ["/newton/raw", "RawData"] ]) eq_(db.stream_list(path="/newton/raw"), [ ["/newton/raw", "RawData"] ])
# Verify that columns were made right # Verify that columns were made right (pytables specific)
eq_(len(db.h5file.getNode("/newton/prep").cols), 9) if "h5file" in db.data.__dict__:
eq_(len(db.h5file.getNode("/newton/raw").cols), 7) h5file = db.data.h5file
eq_(len(db.h5file.getNode("/newton/zzz/rawnotch").cols), 10) eq_(len(h5file.getNode("/newton/prep").cols), 9)
assert(not db.h5file.getNode("/newton/prep").colindexed["timestamp"]) eq_(len(h5file.getNode("/newton/raw").cols), 7)
assert(not db.h5file.getNode("/newton/prep").colindexed["c1"]) eq_(len(h5file.getNode("/newton/zzz/rawnotch").cols), 10)
assert(not h5file.getNode("/newton/prep").colindexed["timestamp"])
assert(not h5file.getNode("/newton/prep").colindexed["c1"])
# Set / get metadata # Set / get metadata
eq_(db.stream_get_metadata("/newton/prep"), {}) eq_(db.stream_get_metadata("/newton/prep"), {})
@@ -196,6 +199,6 @@ class TestServer(object):
# GET instead of POST (no body) # GET instead of POST (no body)
# (actual POST test is done by client code) # (actual POST test is done by client code)
with assert_raises(HTTPError) as e: with assert_raises(HTTPError) as e:
getjson("/stream/insert?path=/newton/prep") getjson("/stream/insert?path=/newton/prep&start=0&end=0")
eq_(e.exception.code, 400) eq_(e.exception.code, 400)

View File

@@ -1,5 +1,5 @@
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
from nose.tools import * from nose.tools import *
from nose.tools import assert_raises from nose.tools import assert_raises

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
from nose.tools import * from nose.tools import *
from nose.tools import assert_raises from nose.tools import assert_raises
@@ -11,65 +11,149 @@ from nilmdb.rbtree import RBTree, RBNode
from test_helpers import * from test_helpers import *
import unittest import unittest
render = False # set to False to skip live renders
do_live_renders = False
def render(tree, description = "", live = True):
import renderdot
r = renderdot.RBTreeRenderer(tree)
return r.render(description, live and do_live_renders)
class TestRBTree: class TestRBTree:
def test_rbtree(self): def test_rbtree(self):
rb = RBTree() rb = RBTree()
rb.insert(RBNode(None, 10000, 10001)) rb.insert(RBNode(10000, 10001))
rb.insert(RBNode(None, 10004, 10007)) rb.insert(RBNode(10004, 10007))
rb.insert(RBNode(None, 10001, 10002)) rb.insert(RBNode(10001, 10002))
s = rb.render_dot()
# There was a typo that gave the RBTree a loop in this case. # There was a typo that gave the RBTree a loop in this case.
# Verify that the dot isn't too big. # Verify that the dot isn't too big.
s = render(rb, live = False)
assert(len(s.splitlines()) < 30) assert(len(s.splitlines()) < 30)
def test_rbtree_big(self): def test_rbtree_big(self):
import random import random
random.seed(1234) random.seed(1234)
# make a set of 500 intervals, inserted in order # make a set of 100 intervals, inserted in order
rb = RBTree() rb = RBTree()
j = 500 j = 100
for i in xrange(j): for i in xrange(j):
rb.insert(RBNode(None, i, i+1)) rb.insert(RBNode(i, i+1))
render(rb, "in-order insert")
# show the graph
if render:
rb.render_dot_live("in-order insert")
# remove about half of them # remove about half of them
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
if random.randint(0,1): if random.randint(0,1):
rb.delete(rb.find(i, i+1)) rb.delete(rb.find(i, i+1))
render(rb, "in-order insert, random delete")
# show the graph # make a set of 100 intervals, inserted at random
if render:
rb.render_dot_live("in-order insert, random delete")
# make a set of 500 intervals, inserted at random
rb = RBTree() rb = RBTree()
j = 500 j = 100
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
rb.insert(RBNode(None, i, i+1)) rb.insert(RBNode(i, i+1))
render(rb, "random insert")
# show the graph
if render:
rb.render_dot_live("random insert")
# remove about half of them # remove about half of them
for i in random.sample(xrange(j),j): for i in random.sample(xrange(j),j):
if random.randint(0,1): if random.randint(0,1):
rb.delete(rb.find(i, i+1)) rb.delete(rb.find(i, i+1))
render(rb, "random insert, random delete")
# show the graph # in-order insert of 50 more
if render: for i in xrange(50):
rb.render_dot_live("random insert, random delete") rb.insert(RBNode(i+500, i+501))
render(rb, "random insert, random delete, in-order insert")
# in-order insert of 250 more def test_rbtree_basics(self):
for i in xrange(250): rb = RBTree()
rb.insert(RBNode(None, i+500, i+501)) vals = [ 7, 14, 1, 2, 8, 11, 5, 15, 4]
for n in vals:
rb.insert(RBNode(n, n))
# show the graph # stringify
if render: s = ""
rb.render_dot_live("random insert, random delete, in-order insert") for node in rb:
s += str(node)
in_("[node (None) 1", s)
eq_(str(rb.nil), "[node nil]")
# inorder traversal, successor and predecessor
last = 0
for node in rb:
assert(node.start > last)
last = node.start
successor = rb.successor(node)
if successor:
assert(rb.predecessor(successor) is node)
predecessor = rb.predecessor(node)
if predecessor:
assert(rb.successor(predecessor) is node)
# Delete node not in the tree
with assert_raises(AttributeError):
rb.delete(RBNode(1,2))
# Delete all nodes!
for node in rb:
rb.delete(node)
# Build it up again, make sure it matches
for n in vals:
rb.insert(RBNode(n, n))
s2 = ""
for node in rb:
s2 += str(node)
assert(s == s2)
def test_rbtree_find(self):
# Get a little bit of coverage for some overlapping cases,
# even though the class doesn't fully support it.
rb = RBTree()
nodes = [ RBNode(1, 5), RBNode(1, 10), RBNode(1, 15) ]
for n in nodes:
rb.insert(n)
assert(rb.find(1, 5) is nodes[0])
assert(rb.find(1, 10) is nodes[1])
assert(rb.find(1, 15) is nodes[2])
def test_rbtree_find_leftright(self):
# Now let's get some ranges in there
rb = RBTree()
vals = [ 7, 14, 1, 2, 8, 11, 5, 15, 4]
for n in vals:
rb.insert(RBNode(n*10, n*10+5))
# Check find_end_left, find_right_start
for i in range(160):
left = rb.find_left_end(i)
right = rb.find_right_start(i)
if left:
# endpoint should be more than i
assert(left.end >= i)
# all earlier nodes should have a lower endpoint
for node in rb:
if node is left:
break
assert(node.end < i)
if right:
# startpoint should be less than i
assert(right.start <= i)
# all later nodes should have a higher startpoint
for node in reversed(list(rb)):
if node is right:
break
assert(node.start > i)
def test_rbtree_intersect(self):
# Fill with some ranges
rb = RBTree()
rb.insert(RBNode(10,20))
rb.insert(RBNode(20,25))
rb.insert(RBNode(30,40))
# Just a quick test; test_interval will do better.
eq_(len(list(rb.intersect(1,100))), 3)
eq_(len(list(rb.intersect(10,20))), 1)
eq_(len(list(rb.intersect(5,15))), 1)
eq_(len(list(rb.intersect(15,15))), 1)
eq_(len(list(rb.intersect(20,21))), 1)
eq_(len(list(rb.intersect(19,21))), 2)

View File

@@ -1,5 +1,5 @@
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import nose import nose
from nose.tools import * from nose.tools import *
@@ -57,7 +57,7 @@ class TestUnserialized(Base):
class TestSerialized(Base): class TestSerialized(Base):
def setUp(self): def setUp(self):
self.realfoo = Foo() self.realfoo = Foo()
self.foo = nilmdb.serializer.WrapObject(self.realfoo) self.foo = nilmdb.utils.Serializer(self.realfoo)
def tearDown(self): def tearDown(self):
del self.foo del self.foo

View File

@@ -1,5 +1,5 @@
import nilmdb import nilmdb
from nilmdb.printf import * from nilmdb.utils.printf import *
import datetime_tz import datetime_tz

View File

@@ -1,20 +1,22 @@
./nilmtool.py destroy /bpnilm/2/raw
./nilmtool.py create /bpnilm/2/raw RawData ./nilmtool.py create /bpnilm/2/raw RawData
if true; then if false; then
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-110000 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s 20110513-120001 -r 8000 /bpnilm/2/raw
else else
for i in $(seq 2000 2050); do # 170 hours, about 98 gigs uncompressed:
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-010001 /bpnilm/2/raw for i in $(seq 2000 2016); do
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-020002 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-010001 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-030003 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-020002 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-040004 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-030003 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-050005 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-040004 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-060006 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-050005 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-070007 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-060006 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-080008 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-070007 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-090009 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-080008 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-100010 /bpnilm/2/raw time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-090009 -r 8000 /bpnilm/2/raw
time zcat /home/jim/bpnilm-data/snapshot-1-20110513-110002.raw.gz | ./nilmtool.py insert -s ${i}0101-100010 -r 8000 /bpnilm/2/raw
done done
fi fi