Compare commits
15 Commits
nilmdb-1.4
...
nilmdb-1.4
Author | SHA1 | Date | |
---|---|---|---|
e01813f29d | |||
7f41e117a2 | |||
dd5fc806e5 | |||
f8ca8d31e6 | |||
ed89d803f0 | |||
3d24092cd2 | |||
304bb43d85 | |||
59a79a30a5 | |||
c0d450d39e | |||
6f14d609b2 | |||
77ef87456f | |||
32d6af935c | |||
6af3a6fc41 | |||
f8a06fb3b7 | |||
e790bb9e8a |
@@ -24,3 +24,5 @@ Usage:
|
||||
|
||||
nilmdb-server --help
|
||||
nilmtool --help
|
||||
|
||||
See docs/wsgi.md for info on setting up a WSGI application in Apache.
|
||||
|
31
docs/wsgi.md
Normal file
31
docs/wsgi.md
Normal file
@@ -0,0 +1,31 @@
|
||||
WSGI Application in Apache
|
||||
--------------------------
|
||||
|
||||
Install `apache2` and `libapache2-mod-wsgi`
|
||||
|
||||
We'll set up the database server at URL `http://myhost.com/nilmdb`.
|
||||
The database will be stored in `/home/nilm/db`, and the process will
|
||||
run as user `nilm`, group `nilm`.
|
||||
|
||||
First, create a WSGI script `/home/nilm/nilmdb.wsgi` containing:
|
||||
|
||||
import nilmdb.server
|
||||
application = nilmdb.server.wsgi_application("/home/nilm/db", "/nilmdb")
|
||||
|
||||
The first parameter is the local filesystem path, and the second
|
||||
parameter is the path part of the URL.
|
||||
|
||||
Then, set up Apache with a configuration like:
|
||||
|
||||
<VirtualHost>
|
||||
WSGIScriptAlias /nilmdb /home/nilm/nilmdb.wsgi
|
||||
WSGIProcessGroup nilmdb-server
|
||||
WSGIDaemonProcess nilmdb-server threads=32 user=nilm group=nilm
|
||||
|
||||
# Access control example:
|
||||
<Location /nilmdb>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
Allow from 1.2.3.4
|
||||
</Location>
|
||||
</VirtualHost>
|
@@ -16,7 +16,7 @@ class HTTPClient(object):
|
||||
reparsed = urlparse.urlparse(baseurl).geturl()
|
||||
if '://' not in reparsed:
|
||||
reparsed = urlparse.urlparse("http://" + baseurl).geturl()
|
||||
self.baseurl = reparsed
|
||||
self.baseurl = reparsed.rstrip('/') + '/'
|
||||
|
||||
# Build Requests session object, enable SSL verification
|
||||
self.session = requests.Session()
|
||||
|
@@ -22,7 +22,7 @@ def main():
|
||||
group.add_argument('-p', '--port', help = 'Listen on the given port',
|
||||
type = int, default = 12380)
|
||||
group.add_argument('-d', '--database', help = 'Database directory',
|
||||
default = os.path.join(os.getcwd(), "db"))
|
||||
default = "./db")
|
||||
group.add_argument('-q', '--quiet', help = 'Silence output',
|
||||
action = 'store_true')
|
||||
group.add_argument('-t', '--traceback',
|
||||
|
@@ -17,5 +17,5 @@ except (ImportError, TypeError): # pragma: no cover
|
||||
pass
|
||||
|
||||
from nilmdb.server.nilmdb import NilmDB
|
||||
from nilmdb.server.server import Server
|
||||
from nilmdb.server.server import Server, wsgi_application
|
||||
from nilmdb.server.errors import NilmDBError, StreamError, OverlapError
|
||||
|
@@ -14,6 +14,7 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import nilmdb.utils.lock
|
||||
from . import rocket
|
||||
|
||||
# Up to 256 open file descriptors at any given time.
|
||||
@@ -26,6 +27,8 @@ class BulkData(object):
|
||||
def __init__(self, basepath, **kwargs):
|
||||
self.basepath = basepath
|
||||
self.root = os.path.join(self.basepath, "data")
|
||||
self.lock = self.root + ".lock"
|
||||
self.lockfile = None
|
||||
|
||||
# Tuneables
|
||||
if "file_size" in kwargs:
|
||||
@@ -44,8 +47,22 @@ class BulkData(object):
|
||||
if not os.path.isdir(self.root):
|
||||
os.mkdir(self.root)
|
||||
|
||||
# Create the lock
|
||||
self.lockfile = open(self.lock, "w")
|
||||
if not nilmdb.utils.lock.exclusive_lock(self.lockfile):
|
||||
raise IOError('database at "' + self.basepath +
|
||||
'" is already locked by another process')
|
||||
|
||||
def close(self):
|
||||
self.getnode.cache_remove_all()
|
||||
if self.lockfile:
|
||||
nilmdb.utils.lock.exclusive_unlock(self.lockfile)
|
||||
self.lockfile.close()
|
||||
try:
|
||||
os.unlink(self.lock)
|
||||
except OSError: # pragma: no cover
|
||||
pass
|
||||
self.lockfile = None
|
||||
|
||||
def _encode_filename(self, path):
|
||||
# Encode all paths to UTF-8, regardless of sys.getfilesystemencoding(),
|
||||
@@ -134,7 +151,7 @@ class BulkData(object):
|
||||
|
||||
# Open and cache it
|
||||
self.getnode(unicodepath)
|
||||
except:
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
try:
|
||||
os.rmdir(ospath)
|
||||
@@ -371,7 +388,7 @@ class Table(object):
|
||||
# Try deleting subdir, too
|
||||
try:
|
||||
os.rmdir(os.path.join(self.root, subdir))
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Cache open files
|
||||
@@ -504,7 +521,7 @@ class Table(object):
|
||||
with open(cachefile, "rb") as f:
|
||||
ranges = pickle.load(f)
|
||||
cachefile_present = True
|
||||
except:
|
||||
except Exception:
|
||||
ranges = []
|
||||
cachefile_present = False
|
||||
|
||||
|
@@ -106,7 +106,9 @@ class NilmDB(object):
|
||||
try:
|
||||
os.makedirs(self.basepath)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
if e.errno != errno.EEXIST: # pragma: no cover
|
||||
# (no coverage, because it's hard to trigger this case
|
||||
# if tests are run as root)
|
||||
raise IOError("can't create tree " + self.basepath)
|
||||
|
||||
# Our data goes inside it
|
||||
@@ -117,8 +119,9 @@ class NilmDB(object):
|
||||
self.con = sqlite3.connect(sqlfilename, check_same_thread = True)
|
||||
try:
|
||||
self._sql_schema_update()
|
||||
finally: # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
self.data.close()
|
||||
raise
|
||||
|
||||
# See big comment at top about the performance implications of this
|
||||
self.con.execute("PRAGMA synchronous=NORMAL")
|
||||
|
@@ -14,6 +14,7 @@ import os
|
||||
import simplejson as json
|
||||
import decorator
|
||||
import psutil
|
||||
import traceback
|
||||
|
||||
class NilmApp(object):
|
||||
def __init__(self, db):
|
||||
@@ -466,7 +467,8 @@ class Server(object):
|
||||
stoppable = False, # whether /exit URL exists
|
||||
embedded = True, # hide diagnostics and output, etc
|
||||
fast_shutdown = False, # don't wait for clients to disconn.
|
||||
force_traceback = False # include traceback in all errors
|
||||
force_traceback = False, # include traceback in all errors
|
||||
basepath = '', # base URL path for cherrypy.tree
|
||||
):
|
||||
# Save server version, just for verification during tests
|
||||
self.version = nilmdb.__version__
|
||||
@@ -526,7 +528,7 @@ class Server(object):
|
||||
if stoppable:
|
||||
root.exit = Exiter()
|
||||
cherrypy.tree.apps = {}
|
||||
cherrypy.tree.mount(root, "/", config = { "/" : app_config })
|
||||
cherrypy.tree.mount(root, basepath, config = { "/" : app_config })
|
||||
|
||||
# Shutdowns normally wait for clients to disconnect. To speed
|
||||
# up tests, set fast_shutdown = True
|
||||
@@ -536,6 +538,9 @@ class Server(object):
|
||||
else:
|
||||
cherrypy.server.shutdown_timeout = 5
|
||||
|
||||
# Set up the WSGI application pointer for external programs
|
||||
self.wsgi_application = cherrypy.tree
|
||||
|
||||
def json_error_page(self, status, message, traceback, version):
|
||||
"""Return a custom error page in JSON so the client can parse it"""
|
||||
errordata = { "status" : status,
|
||||
@@ -602,3 +607,46 @@ class Server(object):
|
||||
|
||||
def stop(self):
|
||||
cherrypy.engine.exit()
|
||||
|
||||
def wsgi_application(dbpath, basepath): # pragma: no cover
|
||||
"""Return a WSGI application object with a database at the
|
||||
specified path.
|
||||
|
||||
'dbpath' is a filesystem location, e.g. /home/nilm/db
|
||||
|
||||
'basepath' is the URL path of the application base, which
|
||||
is the same as the first argument to Apache's WSGIScriptAlias
|
||||
directive.
|
||||
"""
|
||||
server = [None]
|
||||
def application(environ, start_response):
|
||||
if server[0] is None:
|
||||
# Try to start the server
|
||||
try:
|
||||
db = nilmdb.utils.serializer_proxy(nilmdb.server.NilmDB)(dbpath)
|
||||
server[0] = nilmdb.server.Server(
|
||||
db, embedded = True,
|
||||
basepath = basepath.rstrip('/'))
|
||||
except Exception:
|
||||
# Build an error message on failure
|
||||
err = sprintf("Initializing database at path '%s' failed:\n\n",
|
||||
dbpath)
|
||||
err += traceback.format_exc()
|
||||
try:
|
||||
import pwd
|
||||
import grp
|
||||
err += sprintf("\nRunning as: uid=%d (%s), gid=%d (%s)\n",
|
||||
os.getuid(), pwd.getpwuid(os.getuid())[0],
|
||||
os.getgid(), grp.getgrgid(os.getgid())[0])
|
||||
except ImportError:
|
||||
pass
|
||||
if server[0] is None:
|
||||
# Serve up the error with our own mini WSGI app.
|
||||
headers = [ ('Content-type', 'text/plain'),
|
||||
('Content-length', str(len(err))) ]
|
||||
start_response("500 Internal Server Error", headers)
|
||||
return [err]
|
||||
|
||||
# Call the normal application
|
||||
return server[0].wsgi_application(environ, start_response)
|
||||
return application
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""NilmDB utilities"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from nilmdb.utils.timer import Timer
|
||||
from nilmdb.utils.iteratorizer import Iteratorizer
|
||||
from nilmdb.utils.serializer import serializer_proxy
|
||||
from nilmdb.utils.lrucache import lru_cache
|
||||
from nilmdb.utils.diskusage import du, human_size
|
||||
@@ -12,3 +12,4 @@ import nilmdb.utils.fallocate
|
||||
import nilmdb.utils.time
|
||||
import nilmdb.utils.iterator
|
||||
import nilmdb.utils.interval
|
||||
import nilmdb.utils.lock
|
||||
|
@@ -1,100 +0,0 @@
|
||||
import Queue
|
||||
import threading
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
# This file provides a context manager that converts a function
|
||||
# that takes a callback into a generator that returns an iterable.
|
||||
# This is done by running the function in a new thread.
|
||||
|
||||
# Based partially on http://stackoverflow.com/questions/9968592/
|
||||
|
||||
class IteratorizerThread(threading.Thread):
|
||||
def __init__(self, queue, function, curl_hack):
|
||||
"""
|
||||
function: function to execute, which takes the
|
||||
callback (provided by this class) as an argument
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "Iteratorizer-" + function.__name__ + "-" + self.name
|
||||
self.function = function
|
||||
self.queue = queue
|
||||
self.die = False
|
||||
self.curl_hack = curl_hack
|
||||
|
||||
def callback(self, data):
|
||||
try:
|
||||
if self.die:
|
||||
raise Exception() # trigger termination
|
||||
self.queue.put((1, data))
|
||||
except:
|
||||
if self.curl_hack:
|
||||
# We can't raise exceptions, because the pycurl
|
||||
# extension module will unconditionally print the
|
||||
# exception itself, and not pass it up to the caller.
|
||||
# Instead, just return a value that tells curl to
|
||||
# abort. (-1 would be best, in case we were given 0
|
||||
# bytes, but the extension doesn't support that).
|
||||
self.queue.put((2, sys.exc_info()))
|
||||
return 0
|
||||
raise
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
result = self.function(self.callback)
|
||||
except:
|
||||
self.queue.put((2, sys.exc_info()))
|
||||
else:
|
||||
self.queue.put((0, result))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def Iteratorizer(function, curl_hack = False):
|
||||
"""
|
||||
Context manager that takes a function expecting a callback,
|
||||
and provides an iterable that yields the values passed to that
|
||||
callback instead.
|
||||
|
||||
function: function to execute, which takes a callback
|
||||
(provided by this context manager) as an argument
|
||||
|
||||
with iteratorizer(func) as it:
|
||||
for i in it:
|
||||
print 'callback was passed:', i
|
||||
print 'function returned:', it.retval
|
||||
"""
|
||||
queue = Queue.Queue(maxsize = 1)
|
||||
thread = IteratorizerThread(queue, function, curl_hack)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
class iteratorizer_gen(object):
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
self.retval = None
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
(typ, data) = self.queue.get()
|
||||
if typ == 0:
|
||||
# function has returned
|
||||
self.retval = data
|
||||
raise StopIteration
|
||||
elif typ == 1:
|
||||
# data is available
|
||||
return data
|
||||
else:
|
||||
# callback raised an exception
|
||||
raise data[0], data[1], data[2]
|
||||
|
||||
try:
|
||||
yield iteratorizer_gen(queue)
|
||||
finally:
|
||||
# Ask the thread to die, if it's still running.
|
||||
thread.die = True
|
||||
while thread.isAlive():
|
||||
try:
|
||||
queue.get(True, 0.01)
|
||||
except: # pragma: no cover
|
||||
pass
|
33
nilmdb/utils/lock.py
Normal file
33
nilmdb/utils/lock.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# File locking
|
||||
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
import errno
|
||||
|
||||
def exclusive_lock(f):
|
||||
"""Acquire an exclusive lock. Returns True on successful
|
||||
lock, or False on error."""
|
||||
try:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except IOError as e:
|
||||
if e.errno in (errno.EACCES, errno.EAGAIN):
|
||||
return False
|
||||
else: # pragma: no cover
|
||||
raise
|
||||
return True
|
||||
|
||||
def exclusive_unlock(f):
|
||||
"""Release an exclusive lock."""
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
def exclusive_lock(f):
|
||||
"""Dummy lock function -- does not lock!"""
|
||||
warnings.warn("Pretending to lock " + str(f))
|
||||
return True
|
||||
|
||||
def exclusive_unlock(f):
|
||||
"""Release an exclusive lock."""
|
||||
return
|
@@ -15,7 +15,7 @@ def must_close(errorfile = sys.stderr, wrap_verify = False):
|
||||
def wrap_class_method(wrapper):
|
||||
try:
|
||||
orig = getattr(cls, wrapper.__name__).im_func
|
||||
except:
|
||||
except Exception:
|
||||
orig = lambda x: None
|
||||
setattr(cls, wrapper.__name__, decorator.decorator(wrapper, orig))
|
||||
|
||||
|
2
setup.py
2
setup.py
@@ -39,7 +39,7 @@ versioneer.parentdir_prefix = 'nilmdb-'
|
||||
# Hack to workaround logging/multiprocessing issue:
|
||||
# https://groups.google.com/d/msg/nose-users/fnJ-kAUbYHQ/_UsLN786ygcJ
|
||||
try: import multiprocessing
|
||||
except: pass
|
||||
except Exception: pass
|
||||
|
||||
# Use Cython if it's new enough, otherwise use preexisting C files.
|
||||
cython_modules = [ 'nilmdb.server.interval',
|
||||
|
@@ -24,7 +24,7 @@ class JimOrderPlugin(nose.plugins.Plugin):
|
||||
name, workingDir=loader.workingDir)
|
||||
try:
|
||||
order = os.path.join(addr.filename, "test.order")
|
||||
except:
|
||||
except Exception:
|
||||
order = None
|
||||
if order and os.path.exists(order):
|
||||
files = []
|
||||
|
@@ -4,7 +4,6 @@ test_lrucache.py
|
||||
test_mustclose.py
|
||||
|
||||
test_serializer.py
|
||||
test_iteratorizer.py
|
||||
|
||||
test_timestamper.py
|
||||
test_rbtree.py
|
||||
|
@@ -30,6 +30,11 @@ class TestBulkData(object):
|
||||
else:
|
||||
data = BulkData(db, file_size = size, files_per_dir = files)
|
||||
|
||||
# Try opening it again (should result in locking error)
|
||||
with assert_raises(IOError) as e:
|
||||
data2 = BulkData(db)
|
||||
in_("already locked by another process", str(e.exception))
|
||||
|
||||
# create empty
|
||||
with assert_raises(ValueError):
|
||||
data.create("/foo", "uint16_8")
|
||||
|
@@ -619,7 +619,7 @@ class TestClient(object):
|
||||
poolmanager = c.http._last_response.connection.poolmanager
|
||||
pool = poolmanager.pools[('http','localhost',32180)]
|
||||
return (pool.num_connections, pool.num_requests)
|
||||
except:
|
||||
except Exception:
|
||||
raise SkipTest("can't get connection info")
|
||||
|
||||
# First request makes a connection
|
||||
|
@@ -385,7 +385,6 @@ class TestIntervalSpeed:
|
||||
def test_interval_speed(self):
|
||||
import yappi
|
||||
import time
|
||||
import testutil.aplotter as aplotter
|
||||
import random
|
||||
import math
|
||||
|
||||
@@ -406,6 +405,5 @@ class TestIntervalSpeed:
|
||||
speed/j,
|
||||
speed / (j*math.log(j))) # should be constant
|
||||
speeds[j] = speed
|
||||
aplotter.plot(speeds.keys(), speeds.values(), plot_slope=True)
|
||||
yappi.stop()
|
||||
yappi.print_stats(sort_type=yappi.SORTTYPE_TTOT, limit=10)
|
||||
|
@@ -1,61 +0,0 @@
|
||||
import nilmdb
|
||||
from nilmdb.utils.printf import *
|
||||
|
||||
import nose
|
||||
from nose.tools import *
|
||||
from nose.tools import assert_raises
|
||||
import threading
|
||||
import time
|
||||
|
||||
from testutil.helpers import *
|
||||
|
||||
def func_with_callback(a, b, callback):
|
||||
callback(a)
|
||||
callback(b)
|
||||
callback(a+b)
|
||||
return "return value"
|
||||
|
||||
class TestIteratorizer(object):
|
||||
def test(self):
|
||||
|
||||
# First try it with a normal callback
|
||||
self.result = ""
|
||||
def cb(x):
|
||||
self.result += str(x)
|
||||
func_with_callback(1, 2, cb)
|
||||
eq_(self.result, "123")
|
||||
|
||||
# Now make it an iterator
|
||||
result = ""
|
||||
f = lambda x: func_with_callback(1, 2, x)
|
||||
with nilmdb.utils.Iteratorizer(f) as it:
|
||||
for i in it:
|
||||
result += str(i)
|
||||
eq_(result, "123")
|
||||
eq_(it.retval, "return value")
|
||||
|
||||
# Make sure things work when an exception occurs
|
||||
result = ""
|
||||
with nilmdb.utils.Iteratorizer(
|
||||
lambda x: func_with_callback(1, "a", x)) as it:
|
||||
with assert_raises(TypeError) as e:
|
||||
for i in it:
|
||||
result += str(i)
|
||||
eq_(result, "1a")
|
||||
|
||||
# Now try to trigger the case where we stop iterating
|
||||
# mid-generator, and expect the iteratorizer to clean up after
|
||||
# itself. This doesn't have a particular result in the test,
|
||||
# but gains coverage.
|
||||
def foo():
|
||||
with nilmdb.utils.Iteratorizer(f) as it:
|
||||
it.next()
|
||||
foo()
|
||||
eq_(it.retval, None)
|
||||
|
||||
# Do the same thing when the curl hack is applied
|
||||
def foo():
|
||||
with nilmdb.utils.Iteratorizer(f, curl_hack = True) as it:
|
||||
it.next()
|
||||
foo()
|
||||
eq_(it.retval, None)
|
@@ -28,9 +28,6 @@ class Test00Nilmdb(object): # named 00 so it runs first
|
||||
def test_NilmDB(self):
|
||||
recursive_unlink(testdb)
|
||||
|
||||
with assert_raises(IOError):
|
||||
nilmdb.server.NilmDB("/nonexistant-db/foo")
|
||||
|
||||
db = nilmdb.server.NilmDB(testdb)
|
||||
db.close()
|
||||
db = nilmdb.server.NilmDB(testdb)
|
||||
|
@@ -18,7 +18,7 @@ class TestPrintf(object):
|
||||
printf("hello, world: %d", 123)
|
||||
fprintf(test2, "hello too: %d", 123)
|
||||
test3 = sprintf("hello three: %d", 123)
|
||||
except:
|
||||
except Exception:
|
||||
sys.stdout = old_stdout
|
||||
raise
|
||||
sys.stdout = old_stdout
|
||||
|
@@ -1,419 +0,0 @@
|
||||
|
||||
#-----------------------------------------------
|
||||
#aplotter.py - ascii art function plotter
|
||||
#Copyright (c) 2006, Imri Goldberg
|
||||
#All rights reserved.
|
||||
#
|
||||
#Redistribution and use in source and binary forms,
|
||||
#with or without modification, are permitted provided
|
||||
#that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the
|
||||
# above copyright notice, this list of conditions
|
||||
# and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the
|
||||
# above copyright notice, this list of conditions
|
||||
# and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of the <ORGANIZATION> nor the names of
|
||||
# its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
#LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#-----------------------------------------------
|
||||
|
||||
import math
|
||||
|
||||
|
||||
EPSILON = 0.000001
|
||||
|
||||
def transposed(mat):
|
||||
result = []
|
||||
for i in xrange(len(mat[0])):
|
||||
result.append([x[i] for x in mat])
|
||||
return result
|
||||
|
||||
def y_reversed(mat):
|
||||
result = []
|
||||
for i in range(len(mat)):
|
||||
result.append(list(reversed(mat[i])))
|
||||
return result
|
||||
|
||||
def sign(x):
|
||||
if 0<x:
|
||||
return 1
|
||||
if 0 == x:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
class Plotter(object):
|
||||
|
||||
class PlotData(object):
|
||||
def __init__(self, x_size, y_size, min_x, max_x, min_y, max_y, x_mod, y_mod):
|
||||
self.x_size = x_size
|
||||
self.y_size = y_size
|
||||
self.min_x = min_x
|
||||
self.max_x = max_x
|
||||
self.min_y = min_y
|
||||
self.max_y = max_y
|
||||
self.x_mod = x_mod
|
||||
self.y_mod = y_mod
|
||||
|
||||
self.x_step = float(max_x - min_x)/float(self.x_size)
|
||||
self.y_step = float(max_y - min_y)/float(self.y_size)
|
||||
self.inv_x_step = 1/self.x_step
|
||||
self.inv_y_step = 1/self.y_step
|
||||
|
||||
self.ratio = self.y_step / self.x_step
|
||||
def __repr__(self):
|
||||
s = "size: %s, bl: %s, tr: %s, step: %s" % ((self.x_size, self.y_size), (self.min_x, self.min_y), (self.max_x, self.max_y),
|
||||
(self.x_step, self.y_step))
|
||||
return s
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
self.x_size = kwargs.get("x_size", 80)
|
||||
self.y_size = kwargs.get("y_size", 20)
|
||||
|
||||
self.will_draw_axes = kwargs.get("draw_axes", True)
|
||||
|
||||
self.new_line = kwargs.get("newline", "\n")
|
||||
|
||||
self.dot = kwargs.get("dot", "*")
|
||||
|
||||
self.plot_slope = kwargs.get("plot_slope", True)
|
||||
|
||||
self.x_margin = kwargs.get("x_margin", 0.05)
|
||||
self.y_margin = kwargs.get("y_margin", 0.1)
|
||||
|
||||
self.will_plot_labels = kwargs.get("plot_labels", True)
|
||||
|
||||
@staticmethod
|
||||
def get_symbol_by_slope(slope, default_symbol):
|
||||
draw_symbol = default_symbol
|
||||
if slope > math.tan(3*math.pi/8):
|
||||
draw_symbol = "|"
|
||||
elif slope > math.tan(math.pi/8) and slope < math.tan(3*math.pi/8):
|
||||
draw_symbol = "/"
|
||||
elif abs(slope) < math.tan(math.pi/8):
|
||||
draw_symbol = "-"
|
||||
elif slope < math.tan(-math.pi/8) and slope > math.tan(-3*math.pi/8):
|
||||
draw_symbol = "\\"
|
||||
elif slope < math.tan(-3*math.pi/8):
|
||||
draw_symbol = "|"
|
||||
return draw_symbol
|
||||
|
||||
|
||||
def plot_labels(self, output_buffer, plot_data):
|
||||
if plot_data.y_size < 2:
|
||||
return
|
||||
|
||||
margin_factor = 1
|
||||
|
||||
do_plot_x_label = True
|
||||
do_plot_y_label = True
|
||||
|
||||
x_str = "%+g"
|
||||
if plot_data.x_size < 16:
|
||||
do_plot_x_label = False
|
||||
elif plot_data.x_size < 23:
|
||||
x_str = "%+.2g"
|
||||
|
||||
y_str = "%+g"
|
||||
if plot_data.x_size < 8:
|
||||
do_plot_y_label = False
|
||||
elif plot_data.x_size < 11:
|
||||
y_str = "%+.2g"
|
||||
|
||||
act_min_x = (plot_data.min_x + plot_data.x_mod*margin_factor)
|
||||
act_max_x = (plot_data.max_x - plot_data.x_mod*margin_factor)
|
||||
act_min_y = (plot_data.min_y + plot_data.y_mod*margin_factor)
|
||||
act_max_y = (plot_data.max_y - plot_data.y_mod*margin_factor)
|
||||
|
||||
if abs(act_min_x) < 1:
|
||||
min_x_str = "%+.2g" % act_min_x
|
||||
else:
|
||||
min_x_str = x_str % act_min_x
|
||||
|
||||
if abs(act_max_x) < 1:
|
||||
max_x_str = "%+.2g" % act_max_x
|
||||
else:
|
||||
max_x_str = x_str % act_max_x
|
||||
|
||||
if abs(act_min_y) < 1:
|
||||
min_y_str = "%+.2g" % act_min_y
|
||||
else:
|
||||
min_y_str = y_str % act_min_y
|
||||
|
||||
if abs(act_max_y) < 1:
|
||||
max_y_str = "%+.2g" % act_max_y
|
||||
else:
|
||||
max_y_str = y_str % act_max_y
|
||||
|
||||
min_x_coord = self.get_coord(act_min_x,plot_data.min_x,plot_data.x_step)
|
||||
max_x_coord = self.get_coord(act_max_x,plot_data.min_x,plot_data.x_step)
|
||||
min_y_coord = self.get_coord(act_min_y,plot_data.min_y,plot_data.y_step)
|
||||
max_y_coord = self.get_coord(act_max_y,plot_data.min_y,plot_data.y_step)
|
||||
|
||||
|
||||
#print plot_data
|
||||
|
||||
y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step)
|
||||
|
||||
#if plot_data.min_x < 0 and plot_data.max_x > 0:
|
||||
x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step)
|
||||
#else:
|
||||
|
||||
#pass
|
||||
|
||||
output_buffer[x_zero_coord][min_y_coord] = "+"
|
||||
output_buffer[x_zero_coord][max_y_coord] = "+"
|
||||
output_buffer[min_x_coord][y_zero_coord] = "+"
|
||||
output_buffer[max_x_coord][y_zero_coord] = "+"
|
||||
|
||||
if do_plot_x_label:
|
||||
|
||||
for i,c in enumerate(min_x_str):
|
||||
output_buffer[min_x_coord+i][y_zero_coord-1] = c
|
||||
for i,c in enumerate(max_x_str):
|
||||
output_buffer[max_x_coord+i-len(max_x_str)][y_zero_coord-1] = c
|
||||
|
||||
if do_plot_y_label:
|
||||
|
||||
for i,c in enumerate(max_y_str):
|
||||
output_buffer[x_zero_coord+i][max_y_coord] = c
|
||||
for i,c in enumerate(min_y_str):
|
||||
output_buffer[x_zero_coord+i][min_y_coord] = c
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def plot_data(self, xy_seq, output_buffer, plot_data):
|
||||
if self.plot_slope:
|
||||
xy_seq = list(xy_seq)
|
||||
#sort according to the x coord
|
||||
xy_seq.sort(key = lambda c: c[0])
|
||||
prev_p = xy_seq[0]
|
||||
e_xy_seq = enumerate(xy_seq)
|
||||
e_xy_seq.next()
|
||||
for i,(x,y) in e_xy_seq:
|
||||
draw_symbol = self.dot
|
||||
line_drawn = self.plot_line(prev_p, (x,y), output_buffer, plot_data)
|
||||
prev_p = (x,y)
|
||||
if not line_drawn:
|
||||
if i > 0 and i < len(xy_seq)-1:
|
||||
px,py = xy_seq[i-1]
|
||||
nx,ny = xy_seq[i+1]
|
||||
|
||||
if abs(nx-px) > EPSILON:
|
||||
slope = (1.0/plot_data.ratio)*(ny-py)/(nx-px)
|
||||
draw_symbol = self.get_symbol_by_slope(slope, draw_symbol)
|
||||
if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y:
|
||||
continue
|
||||
|
||||
x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step)
|
||||
y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step)
|
||||
if x_coord >= 0 and x_coord < len(output_buffer) and y_coord >= 0 and y_coord < len(output_buffer[0]):
|
||||
if self.draw_axes:
|
||||
if y_coord == self.get_coord(0, plot_data.min_y, plot_data.y_step) and draw_symbol == "-":
|
||||
draw_symbol = "="
|
||||
output_buffer[x_coord][y_coord] = draw_symbol
|
||||
else:
|
||||
for x,y in xy_seq:
|
||||
if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y:
|
||||
continue
|
||||
x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step)
|
||||
y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step)
|
||||
if x_coord >= 0 and x_coord < len(output_buffer) and y_coord > 0 and y_coord < len(output_buffer[0]):
|
||||
output_buffer[x_coord][y_coord] = self.dot
|
||||
|
||||
|
||||
def plot_line(self, start, end, output_buffer, plot_data):
|
||||
|
||||
start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step)
|
||||
end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step)
|
||||
|
||||
x0,y0 = start_coord
|
||||
x1,y1 = end_coord
|
||||
if (x0,y0) == (x1,y1):
|
||||
return True
|
||||
|
||||
clipped_line = clip_line(start, end, (plot_data.min_x, plot_data.min_y), (plot_data.max_x, plot_data.max_y))
|
||||
if clipped_line != None:
|
||||
start,end = clipped_line
|
||||
else:
|
||||
return False
|
||||
start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step)
|
||||
end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step)
|
||||
|
||||
x0,y0 = start_coord
|
||||
x1,y1 = end_coord
|
||||
if (x0,y0) == (x1,y1):
|
||||
return True
|
||||
x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step)
|
||||
y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step)
|
||||
|
||||
if start[0]-end[0] == 0:
|
||||
draw_symbol = "|"
|
||||
else:
|
||||
slope = (1.0/plot_data.ratio)*(end[1]-start[1])/(end[0]-start[0])
|
||||
draw_symbol = self.get_symbol_by_slope(slope, self.dot)
|
||||
try:
|
||||
|
||||
delta = x1-x0, y1-y0
|
||||
if abs(delta[0])>abs(delta[1]):
|
||||
s = sign(delta[0])
|
||||
slope = float(delta[1])/delta[0]
|
||||
for i in range(0,abs(int(delta[0]))):
|
||||
cur_draw_symbol = draw_symbol
|
||||
x = i*s
|
||||
cur_y = int(y0+slope*x)
|
||||
if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-":
|
||||
cur_draw_symbol = "="
|
||||
output_buffer[x0+x][cur_y] = cur_draw_symbol
|
||||
|
||||
|
||||
else:
|
||||
s = sign(delta[1])
|
||||
slope = float(delta[0])/delta[1]
|
||||
for i in range(0,abs(int(delta[1]))):
|
||||
y = i*s
|
||||
cur_draw_symbol = draw_symbol
|
||||
cur_y = y0+y
|
||||
if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-":
|
||||
cur_draw_symbol = "="
|
||||
output_buffer[int(x0+slope*y)][cur_y] = cur_draw_symbol
|
||||
except:
|
||||
print start, end
|
||||
print start_coord, end_coord
|
||||
print plot_data
|
||||
raise
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def plot_single(self, seq, min_x = None, max_x = None, min_y = None, max_y = None):
|
||||
return self.plot_double(range(len(seq)),seq, min_x, max_x, min_y, max_y)
|
||||
|
||||
|
||||
|
||||
|
||||
def plot_double(self, x_seq, y_seq, min_x = None, max_x = None, min_y = None, max_y = None):
|
||||
if min_x == None:
|
||||
min_x = min(x_seq)
|
||||
if max_x == None:
|
||||
max_x = max(x_seq)
|
||||
if min_y == None:
|
||||
min_y = min(y_seq)
|
||||
if max_y == None:
|
||||
max_y = max(y_seq)
|
||||
|
||||
if max_y == min_y:
|
||||
max_y += 1
|
||||
|
||||
x_mod = (max_x-min_x)*self.x_margin
|
||||
y_mod = (max_y-min_y)*self.y_margin
|
||||
min_x-=x_mod
|
||||
max_x+=x_mod
|
||||
min_y-=y_mod
|
||||
max_y+=y_mod
|
||||
|
||||
|
||||
plot_data = self.PlotData(self.x_size, self.y_size, min_x, max_x, min_y, max_y, x_mod, y_mod)
|
||||
|
||||
output_buffer = [[" "]*self.y_size for i in range(self.x_size)]
|
||||
|
||||
if self.will_draw_axes:
|
||||
self.draw_axes(output_buffer, plot_data)
|
||||
|
||||
self.plot_data(zip(x_seq, y_seq), output_buffer, plot_data)
|
||||
|
||||
if self.will_plot_labels:
|
||||
self.plot_labels(output_buffer, plot_data)
|
||||
|
||||
trans_result = transposed(y_reversed(output_buffer))
|
||||
|
||||
result = self.new_line.join(["".join(row) for row in trans_result])
|
||||
return result
|
||||
|
||||
def draw_axes(self, output_buffer, plot_data):
|
||||
|
||||
|
||||
draw_x = False
|
||||
draw_y = False
|
||||
|
||||
if plot_data.min_x <= 0 and plot_data.max_x > 0:
|
||||
draw_y = True
|
||||
zero_x = self.get_coord(0, plot_data.min_x, plot_data.x_step)
|
||||
for y in xrange(plot_data.y_size):
|
||||
output_buffer[zero_x][y] = "|"
|
||||
|
||||
if plot_data.min_y <= 0 and plot_data.max_y > 0:
|
||||
draw_x = True
|
||||
zero_y = self.get_coord(0, plot_data.min_y, plot_data.y_step)
|
||||
for x in xrange(plot_data.x_size):
|
||||
output_buffer[x][zero_y] = "-"
|
||||
|
||||
if draw_x and draw_y:
|
||||
output_buffer[zero_x][zero_y] = "+"
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_coord(val, min, step):
|
||||
result = int((val - min)/step)
|
||||
return result
|
||||
|
||||
def clip_line(line_pt_1, line_pt_2, rect_bottom_left, rect_top_right):
|
||||
ts = [0.0,1.0]
|
||||
if line_pt_1[0] == line_pt_2[0]:
|
||||
return ((line_pt_1[0], max(min(line_pt_1[1], line_pt_2[1]), rect_bottom_left[1])),
|
||||
(line_pt_1[0], min(max(line_pt_1[1], line_pt_2[1]), rect_top_right[1])))
|
||||
if line_pt_1[1] == line_pt_2[1]:
|
||||
return ((max(min(line_pt_1[0], line_pt_2[0]), rect_bottom_left[0]), line_pt_1[1]),
|
||||
(min(max(line_pt_1[0], line_pt_2[0]), rect_top_right[0]), line_pt_1[1]))
|
||||
|
||||
if ((rect_bottom_left[0] <= line_pt_1[0] and line_pt_1[0] < rect_top_right[0]) and
|
||||
(rect_bottom_left[1] <= line_pt_1[1] and line_pt_1[1] < rect_top_right[1]) and
|
||||
(rect_bottom_left[0] <= line_pt_2[0] and line_pt_2[0] < rect_top_right[0]) and
|
||||
(rect_bottom_left[1] <= line_pt_2[1] and line_pt_2[1] < rect_top_right[1])):
|
||||
return line_pt_1, line_pt_2
|
||||
|
||||
ts.append( float(rect_bottom_left[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) )
|
||||
ts.append( float(rect_top_right[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) )
|
||||
ts.append( float(rect_bottom_left[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) )
|
||||
ts.append( float(rect_top_right[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) )
|
||||
|
||||
ts.sort()
|
||||
if ts[2] < 0 or ts[2] >= 1 or ts[3] < 0 or ts[2]>= 1:
|
||||
return None
|
||||
result = [(pt_1 + t*(pt_2-pt_1)) for t in (ts[2],ts[3]) for (pt_1, pt_2) in zip(line_pt_1, line_pt_2)]
|
||||
return (result[0],result[1]), (result[2], result[3])
|
||||
|
||||
|
||||
|
||||
def plot(*args,**flags):
|
||||
limit_flags_names = set(["min_x","min_y","max_x","max_y"])
|
||||
limit_flags = dict([(n,flags[n]) for n in limit_flags_names & set(flags)])
|
||||
settting_flags = dict([(n,flags[n]) for n in set(flags) - limit_flags_names])
|
||||
|
||||
if len(args) == 1:
|
||||
p = Plotter(**settting_flags)
|
||||
print p.plot_single(args[0],**limit_flags)
|
||||
elif len(args) == 2:
|
||||
p = Plotter(**settting_flags)
|
||||
print p.plot_double(args[0],args[1],**limit_flags)
|
||||
else:
|
||||
raise NotImplementedError("can't draw multiple graphs yet")
|
||||
|
||||
__all__ = ["Plotter","plot"]
|
||||
|
Reference in New Issue
Block a user