Compare commits

..

18 Commits

Author SHA1 Message Date
1593e181a3 Switch to versioneer-provided versions everywhere 2013-02-05 19:07:38 -05:00
8e781506de Incorporate versioneer for versioning 2013-02-05 18:49:07 -05:00
f6a2c7620a Restructure cherrypy application more correctly
Specifically, switch from using global configuration and several apps,
to using application-specific configuration with a single app.  This
should hopefully make it easier to plug this into another
WSGI-compliant server someday, and also silences some startup warnings
about missing application configs.
2013-02-04 22:38:49 -05:00
6c30e5ab2f Add gitclean target to Makefile 2013-02-04 22:15:12 -05:00
810eac4e61 Flesh out the list of dependencies in setup.py 2013-02-04 22:14:09 -05:00
d9bb3ab7ab Fix iteratorizer coverage issue with thread timing 2013-02-04 22:14:01 -05:00
21d0e90bd9 Rework Cython and external module support.
Now we build Cython modules only if cython >= 0.16 is present.
Tarballs made by "make sdist" include the Cython-generated *.c files,
and so Cython isn't required on the end user machine at all.
2013-02-04 22:12:52 -05:00
f071d749ce Generate a MANIFEST.in from setup.py; more setup.py and Makefile updates 2013-02-04 18:14:44 -05:00
d95c354595 Print a warning in setup.py if basic dependencies aren't present 2013-02-01 17:44:54 -05:00
9bcd8183f6 Add cython dependency 2013-02-01 17:44:27 -05:00
5c531d8273 Convert runserver.py into a generated nilmdb-server script 2013-02-01 17:43:41 -05:00
3fe3e2ca95 Move nilmtool into a dedicated nilmdb.scripts module 2013-02-01 17:42:09 -05:00
f01e781469 Convert nilmtool.py into a setuptools-generated script
At install time, the script "/usr/bin/nilmtool" will be created.
2013-02-01 16:25:12 -05:00
e6180a5a81 Remove all relative imports 2013-02-01 16:02:01 -05:00
a9d31b46ed More files in clean target 2013-02-01 15:48:55 -05:00
b01f23ed99 Move runtests.py script into test directory 2013-02-01 15:47:47 -05:00
842bf21411 Include the full server response if we can't parse errors out of it.
This makes things easier to debug if there's an error in
e.g. json_error_handler(), or if we're trying to poke a server that's
not even ours.
2013-02-01 15:47:47 -05:00
750d9e3c38 Clean up some pylint warnings and potential errors 2013-02-01 15:29:24 -05:00
39 changed files with 1449 additions and 180 deletions

View File

@@ -7,4 +7,4 @@
exclude_lines =
pragma: no cover
if 0:
omit = nilmdb/utils/datetime_tz*
omit = nilmdb/utils/datetime_tz*,nilmdb/scripts,nilmdb/_version.py

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
nilmdb/_version.py export-subst

4
.gitignore vendored
View File

@@ -18,6 +18,10 @@ nilmdb/server/rbtree.so
dist/
nilmdb.egg-info/
# This gets generated as needed by setup.py
MANIFEST.in
MANIFEST
# Misc
timeit*out

250
.pylintrc Normal file
View File

@@ -0,0 +1,250 @@
# -*- conf -*-
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=datetime_tz
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=C0111,R0903,R0201,R0914,R0912,W0142,W0703,W0702
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=parseable
# Include message's id in output
include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the beginning of the name of dummy variables
# (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|version)$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{0,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{0,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{0,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{0,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@@ -1,12 +1,36 @@
# By default, run the tests.
all: test
version:
python setup.py version
build:
python setup.py build_ext --inplace
dist: sdist
sdist:
python setup.py sdist
install:
python setup.py install
docs:
make -C docs
lint:
pylint -f parseable nilmdb
pylint --rcfile=.pylintrc nilmdb
test:
python runtests.py
python tests/runtests.py
clean::
find . -name '*pyc' | xargs rm -f
rm -f .coverage
rm -rf tests/*testdb*
rm -rf nilmdb.egg-info/ build/ nilmdb/server/*.so MANIFEST.in
make -C docs clean
gitclean::
git clean -dXf
.PHONY: all build dist sdist install docs lint test clean

View File

@@ -3,8 +3,20 @@ by Jim Paris <jim@jtan.com>
Prerequisites:
sudo apt-get install python2.7 python-cherrypy3 python-decorator python-nose python-coverage python-setuptools
# Runtime and build environments
sudo apt-get install python2.7 python2.7-dev python-setuptools cython
# Base NilmDB dependencies
sudo apt-get install python-cherrypy3 python-decorator python-simplejson python-pycurl python-dateutil python-tz
# Tools for running tests
sudo apt-get install python-nose python-coverage
Install:
python setup.py install
Usage:
nilmdb-server --help
nilmtool --help

View File

@@ -1,4 +1,8 @@
"""Main NilmDB import"""
from server import NilmDB, Server
from client import Client
from nilmdb.server import NilmDB, Server
from nilmdb.client import Client
from nilmdb._version import get_versions
__version__ = get_versions()['version']
del get_versions

197
nilmdb/_version.py Normal file
View File

@@ -0,0 +1,197 @@
IN_LONG_VERSION_PY = True
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (build by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.7+ (https://github.com/warner/python-versioneer)
# these strings will be replaced by git during git-archive
git_refnames = "$Format:%d$"
git_full = "$Format:%H$"
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %s" % args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%s', no digits" % ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %s" % ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" %
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
tag_prefix = "nilmdb-"
parentdir_prefix = "nilmdb-"
versionfile_source = "nilmdb/_version.py"
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
variables = { "refnames": git_refnames, "full": git_full }
ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
if not ver:
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if not ver:
ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
verbose)
if not ver:
ver = default
return ver

View File

@@ -1,4 +1,4 @@
"""nilmdb.client"""
from .client import Client
from .errors import *
from nilmdb.client.client import Client
from nilmdb.client.errors import ClientError, ServerError, Error

View File

@@ -5,26 +5,17 @@
import nilmdb
import nilmdb.utils
import nilmdb.client.httpclient
from nilmdb.utils.printf import *
import time
import sys
import re
import os
import simplejson as json
import itertools
version = "1.0"
def float_to_string(f):
# Use repr to maintain full precision in the string output.
"""Use repr to maintain full precision in the string output."""
return repr(float(f))
class Client(object):
"""Main client interface to the Nilm database."""
client_version = version
def __init__(self, url):
self.http = nilmdb.client.httpclient.HTTPClient(url)
@@ -151,6 +142,8 @@ class Client(object):
block_data = ""
block_start = start
result = None
line = None
nextline = None
for (line, nextline) in nilmdb.utils.misc.pairwise(data):
# If we don't have a starting time, extract it from the first line
if block_start is None:

View File

@@ -2,13 +2,8 @@
import nilmdb
import nilmdb.utils
from nilmdb.utils.printf import *
from nilmdb.client.errors import *
from nilmdb.client.errors import ClientError, ServerError, Error
import time
import sys
import re
import os
import simplejson as json
import urlparse
import pycurl
@@ -42,10 +37,12 @@ class HTTPClient(object):
code = self.curl.getinfo(pycurl.RESPONSE_CODE)
if code == 200:
return
# Default variables for exception
# Default variables for exception. We use the entire body as
# the default message, in case we can't extract it from a JSON
# response.
args = { "url" : self.url,
"status" : str(code),
"message" : None,
"message" : body,
"traceback" : None }
try:
# Fill with server-provided data if we can

View File

@@ -1,3 +1,3 @@
"""nilmdb.cmdline"""
from .cmdline import Cmdline
from nilmdb.cmdline.cmdline import Cmdline

View File

@@ -4,21 +4,17 @@ import nilmdb
from nilmdb.utils.printf import *
from nilmdb.utils import datetime_tz
import dateutil.parser
import sys
import re
import argparse
from argparse import ArgumentDefaultsHelpFormatter as def_form
version = "1.0"
# Valid subcommands. Defined in separate files just to break
# things up -- they're still called with Cmdline as self.
subcommands = [ "info", "create", "list", "metadata", "insert", "extract",
"remove", "destroy" ]
# Import the subcommand modules. Equivalent way of doing this would be
# from . import info as cmd_info
# Import the subcommand modules
subcmd_mods = {}
for cmd in subcommands:
subcmd_mods[cmd] = __import__("nilmdb.cmdline." + cmd, fromlist = [ cmd ])
@@ -30,8 +26,8 @@ class JimArgumentParser(argparse.ArgumentParser):
class Cmdline(object):
def __init__(self, argv):
self.argv = argv
def __init__(self, argv = None):
self.argv = argv or sys.argv[1:]
self.client = None
def arg_time(self, toparse):
@@ -95,9 +91,6 @@ class Cmdline(object):
return dt.strftime("%a, %d %b %Y %H:%M:%S.%f %z")
def parser_setup(self):
version_string = sprintf("nilmtool %s, client library %s",
version, nilmdb.Client.client_version)
self.parser = JimArgumentParser(add_help = False,
formatter_class = def_form)
@@ -105,7 +98,7 @@ class Cmdline(object):
group.add_argument("-h", "--help", action='help',
help='show this help message and exit')
group.add_argument("-V", "--version", action="version",
version=version_string)
version = nilmdb.__version__)
group = self.parser.add_argument_group("Server")
group.add_argument("-u", "--url", action="store",

View File

@@ -1,9 +1,7 @@
from nilmdb.utils.printf import *
import nilmdb
import nilmdb.client
import textwrap
from argparse import ArgumentDefaultsHelpFormatter as def_form
from argparse import RawDescriptionHelpFormatter as raw_form
def setup(self, sub):

View File

@@ -1,8 +1,6 @@
from __future__ import print_function
from nilmdb.utils.printf import *
import nilmdb
import nilmdb.client
import sys
def setup(self, sub):
cmd = sub.add_parser("extract", help="Extract data",

View File

@@ -1,3 +1,4 @@
import nilmdb
from nilmdb.utils.printf import *
from argparse import ArgumentDefaultsHelpFormatter as def_form
@@ -13,7 +14,7 @@ def setup(self, sub):
def cmd_info(self):
"""Print info about the server"""
printf("Client library version: %s\n", self.client.client_version)
printf("Client version: %s\n", nilmdb.__version__)
printf("Server version: %s\n", self.client.version())
printf("Server URL: %s\n", self.client.geturl())
printf("Server database path: %s\n", self.client.dbpath())

View File

@@ -53,8 +53,6 @@ def cmd_insert(self):
if len(streams) != 1:
self.die("error getting stream info for path %s", self.args.path)
layout = streams[0][1]
if self.args.start and len(self.args.file) != 1:
self.die("error: --start can only be used with one input file")
@@ -93,7 +91,7 @@ def cmd_insert(self):
# Insert the data
try:
result = self.client.stream_insert(self.args.path, ts)
self.client.stream_insert(self.args.path, ts)
except nilmdb.client.Error as e:
# TODO: It would be nice to be able to offer better errors
# here, particularly in the case of overlap, which just shows

View File

@@ -1,6 +1,4 @@
from nilmdb.utils.printf import *
import nilmdb
import nilmdb.client
import fnmatch
import argparse

View File

@@ -1,7 +1,6 @@
from nilmdb.utils.printf import *
import nilmdb
import nilmdb.client
import sys
def setup(self, sub):
cmd = sub.add_parser("remove", help="Remove data",

View File

@@ -0,0 +1 @@
# Command line scripts

81
nilmdb/scripts/nilmdb_server.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/python
import nilmdb.server
import argparse
import os
import socket
def main():
"""Main entry point for the 'nilmdb-server' command line script"""
parser = argparse.ArgumentParser(
description = 'Run the NilmDB server',
formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-V", "--version", action="version",
version = nilmdb.__version__)
group = parser.add_argument_group("Standard options")
group.add_argument('-a', '--address',
help = 'Only listen on the given address',
default = '0.0.0.0')
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"))
group.add_argument('-q', '--quiet', help = 'Silence output',
action = 'store_true')
group = parser.add_argument_group("Debug options")
group.add_argument('-y', '--yappi', help = 'Run under yappi profiler and '
'invoke interactive shell afterwards',
action = 'store_true')
args = parser.parse_args()
# Create database object
db = nilmdb.server.NilmDB(args.database)
# Configure the server
if args.quiet:
embedded = True
else:
embedded = False
server = nilmdb.server.Server(db,
host = args.address,
port = args.port,
embedded = embedded)
# Print info
if not args.quiet:
print "Database: %s" % (os.path.realpath(args.database))
if args.address == '0.0.0.0' or args.address == '::':
host = socket.getfqdn()
else:
host = args.address
print "Server URL: http://%s:%d/" % ( host, args.port)
print "----"
# Run it
if args.yappi:
print "Running in yappi"
try:
import yappi
yappi.start()
server.start(blocking = True)
finally:
yappi.stop()
yappi.print_stats(sort_type = yappi.SORTTYPE_TTOT, limit = 50)
from IPython import embed
embed(header = "Use the yappi object to explore further, "
"quit to exit")
else:
server.start(blocking = True)
# Clean up
if not args.quiet:
print "Closing database"
db.close()
if __name__ == "__main__":
main()

10
nilmdb/scripts/nilmtool.py Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/python
import nilmdb.cmdline
def main():
"""Main entry point for the 'nilmtool' command line script"""
nilmdb.cmdline.Cmdline().run()
if __name__ == "__main__":
main()

View File

@@ -1,15 +1,22 @@
"""nilmdb.server"""
from __future__ import absolute_import
# Try to set up pyximport to automatically rebuild Cython modules. If
# this doesn't work, it's OK, as long as the modules were built externally.
# (e.g. python setup.py build_ext --inplace)
try:
import Cython
import distutils.version
if (distutils.version.LooseVersion(Cython.__version__) <
distutils.version.LooseVersion("0.16")): # pragma: no cover
raise ImportError("Cython version too old")
import pyximport
pyximport.install()
import layout
except: # pragma: no cover
pyximport.install(inplace = True, build_in_temp = False)
except ImportError: # pragma: no cover
pass
from .nilmdb import NilmDB
from .server import Server
from .errors import *
import nilmdb.server.layout
from nilmdb.server.nilmdb import NilmDB
from nilmdb.server.server import Server
from nilmdb.server.errors import NilmDBError, StreamError, OverlapError

View File

@@ -8,10 +8,8 @@ import nilmdb
from nilmdb.utils.printf import *
import os
import sys
import cPickle as pickle
import struct
import fnmatch
import mmap
import re
@@ -91,8 +89,7 @@ class BulkData(object):
"float32": 'f',
"float64": 'd',
}
for n in range(layout.count):
struct_fmt += struct_mapping[layout.datatype]
struct_fmt += struct_mapping[layout.datatype] * layout.count
except KeyError:
raise ValueError("no such layout, or bad data types")
@@ -185,12 +182,12 @@ class Table(object):
packer = struct.Struct(struct_fmt)
rows_per_file = max(file_size // packer.size, 1)
format = { "rows_per_file": rows_per_file,
"files_per_dir": files_per_dir,
"struct_fmt": struct_fmt,
"version": 1 }
fmt = { "rows_per_file": rows_per_file,
"files_per_dir": files_per_dir,
"struct_fmt": struct_fmt,
"version": 1 }
with open(os.path.join(root, "_format"), "wb") as f:
pickle.dump(format, f, 2)
pickle.dump(fmt, f, 2)
# Normal methods
def __init__(self, root):
@@ -199,15 +196,15 @@ class Table(object):
# Load the format and build packer
with open(os.path.join(self.root, "_format"), "rb") as f:
format = pickle.load(f)
fmt = pickle.load(f)
if format["version"] != 1: # pragma: no cover (just future proofing)
raise NotImplementedError("version " + format["version"] +
if fmt["version"] != 1: # pragma: no cover (just future proofing)
raise NotImplementedError("version " + fmt["version"] +
" bulk data store not supported")
self.rows_per_file = format["rows_per_file"]
self.files_per_dir = format["files_per_dir"]
self.packer = struct.Struct(format["struct_fmt"])
self.rows_per_file = fmt["rows_per_file"]
self.files_per_dir = fmt["files_per_dir"]
self.packer = struct.Struct(fmt["struct_fmt"])
self.file_size = self.packer.size * self.rows_per_file
# Find nrows
@@ -278,7 +275,7 @@ class Table(object):
# Cache open files
@nilmdb.utils.lru_cache(size = fd_cache_size,
keys = slice(0,3), # exclude newsize
keys = slice(0, 3), # exclude newsize
onremove = lambda x: x.close())
def mmap_open(self, subdir, filename, newsize = None):
"""Open and map a given 'subdir/filename' (relative to self.root).

View File

@@ -15,11 +15,9 @@ from nilmdb.utils.printf import *
from nilmdb.server.interval import (Interval, DBInterval,
IntervalSet, IntervalError)
from nilmdb.server import bulkdata
from nilmdb.server.errors import *
from nilmdb.server.errors import NilmDBError, StreamError, OverlapError
import sqlite3
import time
import sys
import os
import errno
import bisect
@@ -80,7 +78,10 @@ class NilmDB(object):
verbose = 0
def __init__(self, basepath, sync=True, max_results=None,
bulkdata_args={}):
bulkdata_args=None):
if bulkdata_args is None:
bulkdata_args = {}
# set up path
self.basepath = os.path.abspath(basepath)
@@ -156,7 +157,7 @@ class NilmDB(object):
iset += DBInterval(start_time, end_time,
start_time, end_time,
start_pos, end_pos)
except IntervalError as e: # pragma: no cover
except IntervalError: # pragma: no cover
raise NilmDBError("unexpected overlap in ranges table!")
return iset

View File

@@ -5,18 +5,16 @@
from __future__ import absolute_import
import nilmdb
from nilmdb.utils.printf import *
from nilmdb.server.errors import *
from nilmdb.server.errors import NilmDBError
import cherrypy
import sys
import time
import os
import simplejson as json
import decorator
import traceback
try:
import cherrypy
cherrypy.tools.json_out
except: # pragma: no cover
sys.stderr.write("Cherrypy 3.2+ required\n")
@@ -26,8 +24,6 @@ class NilmApp(object):
def __init__(self, db):
self.db = db
version = "1.2"
# Decorators
def chunked_response(func):
"""Decorator to enable chunked responses."""
@@ -57,7 +53,7 @@ def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover
try:
for val in func(*args, **kwargs):
yield val
except (LookupError, UnicodeError) as e:
except (LookupError, UnicodeError):
raise Exception("bug workaround; real exception is:\n" +
traceback.format_exc())
@@ -84,9 +80,8 @@ def exception_to_httperror(*expected):
class Root(NilmApp):
"""Root application for NILM database"""
def __init__(self, db, version):
def __init__(self, db):
super(Root, self).__init__(db)
self.server_version = version
# /
@cherrypy.expose
@@ -102,7 +97,7 @@ class Root(NilmApp):
@cherrypy.expose
@cherrypy.tools.json_out()
def version(self):
return self.server_version
return nilmdb.__version__
# /dbpath
@cherrypy.expose
@@ -247,7 +242,7 @@ class Stream(NilmApp):
# Now do the nilmdb insert, passing it the parser full of data.
try:
result = self.db.stream_insert(path, start, end, parser.data)
self.db.stream_insert(path, start, end, parser.data)
except NilmDBError as e:
raise cherrypy.HTTPError("400 Bad Request", e.message)
@@ -309,7 +304,7 @@ class Stream(NilmApp):
def content(start, end):
# Note: disable chunked responses to see tracebacks from here.
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 ])
yield response
if restart == 0:
@@ -387,31 +382,39 @@ class Server(object):
fast_shutdown = False, # don't wait for clients to disconn.
force_traceback = False # include traceback in all errors
):
self.version = version
# Save server version, just for verification during tests
self.version = nilmdb.__version__
# Need to wrap DB object in a serializer because we'll call
# into it from separate threads.
self.embedded = embedded
self.db = nilmdb.utils.Serializer(db)
# Build up global server configuration
cherrypy.config.update({
'server.socket_host': host,
'server.socket_port': port,
'engine.autoreload_on': False,
'server.max_request_body_size': 4*1024*1024,
'error_page.default': self.json_error_page,
})
if self.embedded:
cherrypy.config.update({ 'environment': 'embedded' })
# Build up application specific configuration
app_config = {}
app_config.update({
'error_page.default': self.json_error_page,
})
# Send a permissive Access-Control-Allow-Origin (CORS) header
# with all responses so that browsers can send cross-domain
# requests to this server.
cherrypy.config.update({ 'response.headers.Access-Control-Allow-Origin':
'*' })
app_config.update({ 'response.headers.Access-Control-Allow-Origin':
'*' })
# Send tracebacks in error responses. They're hidden by the
# error_page function for client errors (code 400-499).
cherrypy.config.update({ 'request.show_tracebacks' : True })
app_config.update({ 'request.show_tracebacks' : True })
self.force_traceback = force_traceback
# Patch CherryPy error handler to never pad out error messages.
@@ -419,11 +422,13 @@ class Server(object):
# error messages.
cherrypy._cperror._ie_friendly_error_sizes = {}
cherrypy.tree.apps = {}
cherrypy.tree.mount(Root(self.db, self.version), "/")
cherrypy.tree.mount(Stream(self.db), "/stream")
# Build up the application and mount it
root = Root(self.db)
root.stream = Stream(self.db)
if stoppable:
cherrypy.tree.mount(Exiter(), "/exit")
root.exit = Exiter()
cherrypy.tree.apps = {}
cherrypy.tree.mount(root, "/", config = { "/" : app_config })
# Shutdowns normally wait for clients to disconnect. To speed
# up tests, set fast_shutdown = True
@@ -444,7 +449,7 @@ class Server(object):
if not self.force_traceback:
if code >= 400 and code <= 499:
errordata["traceback"] = ""
except Exception as e: # pragma: no cover
except Exception: # pragma: no cover
pass
# Override the response type, which was previously set to text/html
cherrypy.serving.response.headers['Content-Type'] = (

View File

@@ -1,11 +1,11 @@
"""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
from . import misc
from . import atomic
from nilmdb.utils.timer import Timer
from nilmdb.utils.iteratorizer import Iteratorizer
from nilmdb.utils.serializer import Serializer
from nilmdb.utils.lrucache import lru_cache
from nilmdb.utils.diskusage import du
from nilmdb.utils.mustclose import must_close
from nilmdb.utils.urllib import urlencode
from nilmdb.utils import misc
from nilmdb.utils import atomic

View File

@@ -19,8 +19,8 @@ def du_bytes(path):
"""Like du -sb, returns total size of path in bytes."""
size = os.path.getsize(path)
if os.path.isdir(path):
for file in os.listdir(path):
filepath = os.path.join(path, file)
for thisfile in os.listdir(path):
filepath = os.path.join(path, thisfile)
size += du_bytes(filepath)
return size

View File

@@ -95,5 +95,5 @@ def Iteratorizer(function, curl_hack = False):
while thread.isAlive():
try:
queue.get(True, 0.01)
except:
except: # pragma: no cover
pass

View File

@@ -5,7 +5,6 @@
import collections
import decorator
import warnings
def lru_cache(size = 10, onremove = None, keys = slice(None)):
"""Least-recently-used cache decorator.

View File

@@ -2,7 +2,7 @@
# Simple timer to time a block of code, for optimization debugging
# use like:
# with nilmdb.Timer("flush"):
# with nilmdb.utils.Timer("flush"):
# foo.flush()
from __future__ import print_function

View File

@@ -3,19 +3,16 @@
from nilmdb.utils.printf import *
from nilmdb.utils import datetime_tz
import time
import os
class Timestamper(object):
"""A file-like object that adds timestamps to lines of an input file."""
def __init__(self, file, ts_iter):
def __init__(self, infile, ts_iter):
"""file: filename, or another file-like object
ts_iter: iterator that returns a timestamp string for
each line of the file"""
if isinstance(file, basestring):
self.file = open(file, "r")
if isinstance(infile, basestring):
self.file = open(infile, "r")
else:
self.file = file
self.file = infile
self.ts_iter = ts_iter
def close(self):
@@ -54,7 +51,7 @@ class Timestamper(object):
class TimestamperRate(Timestamper):
"""Timestamper that uses a start time and a fixed rate"""
def __init__(self, file, start, rate, end = None):
def __init__(self, infile, start, rate, end = None):
"""
file: file name or object
@@ -76,7 +73,7 @@ class TimestamperRate(Timestamper):
# Handle case where we're passed a datetime or datetime_tz object
if "totimestamp" in dir(start):
start = start.totimestamp()
Timestamper.__init__(self, file, iterator(start, rate, end))
Timestamper.__init__(self, infile, iterator(start, rate, end))
self.start = start
self.rate = rate
def __str__(self):
@@ -87,21 +84,21 @@ class TimestamperRate(Timestamper):
class TimestamperNow(Timestamper):
"""Timestamper that uses current time"""
def __init__(self, file):
def __init__(self, infile):
def iterator():
while True:
now = datetime_tz.datetime_tz.utcnow().totimestamp()
yield sprintf("%.6f ", now)
Timestamper.__init__(self, file, iterator())
Timestamper.__init__(self, infile, iterator())
def __str__(self):
return "TimestamperNow(...)"
class TimestamperNull(Timestamper):
"""Timestamper that adds nothing to each line"""
def __init__(self, file):
def __init__(self, infile):
def iterator():
while True:
yield ""
Timestamper.__init__(self, file, iterator())
Timestamper.__init__(self, infile, iterator())
def __str__(self):
return "TimestamperNull(...)"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/python
import nilmdb
import sys
nilmdb.cmdline.Cmdline(sys.argv[1:]).run()

View File

@@ -1,35 +0,0 @@
#!/usr/bin/python
import nilmdb
import argparse
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('-d', '--database', help='Database directory', default="db")
parser.add_argument('-y', '--yappi', help='Run with yappi profiler',
action='store_true')
args = parser.parse_args()
# Start web app on a custom port
db = nilmdb.NilmDB(args.database)
server = nilmdb.Server(db, host = "127.0.0.1",
port = args.port,
embedded = False)
if args.yappi:
print "Running in yappi"
try:
import yappi
yappi.start()
server.start(blocking = True)
finally:
yappi.stop()
print "Try: yappi.print_stats(sort_type=yappi.SORTTYPE_TTOT,limit=50)"
from IPython import embed
embed()
else:
server.start(blocking = True)
db.close()

108
setup.py
View File

@@ -1,5 +1,11 @@
#!/usr/bin/python
# To release a new version, tag it:
# git tag -a nilmdb-1.1 -m "Version 1.1"
# git push --tags
# Then just package it up:
# python setup.py sdist
# This is supposed to be using Distribute:
#
# distutils provides a "setup" method.
@@ -9,32 +15,105 @@
# So we don't really know if this is using the old setuptools or the
# Distribute-provided version of setuptools.
from setuptools import setup, find_packages
from distutils.extension import Extension
import traceback
import sys
import os
from Cython.Build import cythonize
try:
from setuptools import setup, find_packages
from distutils.extension import Extension
import distutils.version
except ImportError:
traceback.print_exc()
print "Please install the prerequisites listed in README.txt"
sys.exit(1)
# Versioneer manages version numbers from git tags.
# https://github.com/warner/python-versioneer
import versioneer
versioneer.versionfile_source = 'nilmdb/_version.py'
versioneer.versionfile_build = 'nilmdb/_version.py'
versioneer.tag_prefix = 'nilmdb-'
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
# Build cython modules.
cython_modules = cythonize("**/*.pyx")
# Use Cython if it's new enough, otherwise use preexisting C files.
cython_modules = [ 'nilmdb.server.interval',
'nilmdb.server.layout',
'nilmdb.server.rbtree' ]
try:
import Cython
from Cython.Build import cythonize
if (distutils.version.LooseVersion(Cython.__version__) <
distutils.version.LooseVersion("0.16")):
print "Cython version", Cython.__version__, "is too old; not using it."
raise ImportError()
use_cython = True
except ImportError:
use_cython = False
ext_modules = []
for modulename in cython_modules:
filename = modulename.replace('.','/')
if use_cython:
ext_modules.extend(cythonize(filename + ".pyx"))
else:
cfile = filename + ".c"
if not os.path.exists(cfile):
raise Exception("Missing source file " + cfile + ". "
"Try installing cython >= 0.16.")
ext_modules.append(Extension(modulename, [ cfile ]))
# We need a MANIFEST.in. Generate it here rather than polluting the
# repository with yet another setup-related file.
with open("MANIFEST.in", "w") as m:
m.write("""
# Root
include README.txt
include setup.cfg
include setup.py
include versioneer.py
include Makefile
include .coveragerc
include .pylintrc
# Cython files -- include source.
recursive-include nilmdb/server *.pyx *.pyxdep *.pxd
# Tests
recursive-include tests *.py
recursive-include tests/data *
include tests/test.order
# Docs
recursive-include docs Makefile *.md
""")
# Run setup
setup(name='nilmdb',
version = '1.0',
version = versioneer.get_version(),
cmdclass = versioneer.get_cmdclass(),
url = 'https://git.jim.sh/jim/lees/nilmdb.git',
author = 'Jim Paris',
description = "NILM Database",
long_description = "NILM Database",
license = "Proprietary",
author_email = 'jim@jtan.com',
tests_require = [ 'nose',
'coverage',
],
setup_requires = [ 'cython',
],
install_requires = [ 'distribute',
'decorator',
setup_requires = [ 'distribute',
],
install_requires = [ 'decorator',
'cherrypy >= 3.2',
'simplejson',
'pycurl',
'python-dateutil',
'pytz',
],
packages = [ 'nilmdb',
'nilmdb.utils',
@@ -42,7 +121,14 @@ setup(name='nilmdb',
'nilmdb.server',
'nilmdb.client',
'nilmdb.cmdline',
'nilmdb.scripts',
],
ext_modules = cython_modules,
entry_points = {
'console_scripts': [
'nilmtool = nilmdb.scripts.nilmtool:main',
'nilmdb-server = nilmdb.scripts.nilmdb_server:main',
],
},
ext_modules = ext_modules,
zip_safe = False,
)

View File

@@ -6,6 +6,9 @@ import sys
import glob
from collections import OrderedDict
# Change into parent dir
os.chdir(os.path.dirname(os.path.realpath(__file__)) + "/..")
class JimOrderPlugin(nose.plugins.Plugin):
"""When searching for tests and encountering a directory that
contains a 'test.order' file, run tests listed in that file, in the

View File

@@ -67,8 +67,8 @@ class TestClient(object):
# Now use the real URL
client = nilmdb.Client(url = "http://localhost:12380/")
version = client.version()
eq_(distutils.version.StrictVersion(version),
distutils.version.StrictVersion(test_server.version))
eq_(distutils.version.LooseVersion(version),
distutils.version.LooseVersion(test_server.version))
# Bad URLs should give 404, not 500
with assert_raises(ClientError):

View File

@@ -151,8 +151,8 @@ class TestServer(object):
eq_(e.exception.code, 404)
# Check version
eq_(distutils.version.StrictVersion(getjson("/version")),
distutils.version.StrictVersion(self.server.version))
eq_(distutils.version.LooseVersion(getjson("/version")),
distutils.version.LooseVersion(nilmdb.__version__))
def test_stream_list(self):
# Known streams that got populated by an earlier test (test_nilmdb)

656
versioneer.py Normal file
View File

@@ -0,0 +1,656 @@
#! /usr/bin/python
"""versioneer.py
(like a rocketeer, but for versions)
* https://github.com/warner/python-versioneer
* Brian Warner
* License: Public Domain
* Version: 0.7+
This file helps distutils-based projects manage their version number by just
creating version-control tags.
For developers who work from a VCS-generated tree (e.g. 'git clone' etc),
each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a
version number by asking your version-control tool about the current
checkout. The version number will be written into a generated _version.py
file of your choosing, where it can be included by your __init__.py
For users who work from a VCS-generated tarball (e.g. 'git archive'), it will
compute a version number by looking at the name of the directory created when
te tarball is unpacked. This conventionally includes both the name of the
project and a version number.
For users who work from a tarball built by 'setup.py sdist', it will get a
version number from a previously-generated _version.py file.
As a result, loading code directly from the source tree will not result in a
real version. If you want real versions from VCS trees (where you frequently
update from the upstream repository, or do new development), you will need to
do a 'setup.py version' after each update, and load code from the build/
directory.
You need to provide this code with a few configuration values:
versionfile_source:
A project-relative pathname into which the generated version strings
should be written. This is usually a _version.py next to your project's
main __init__.py file. If your project uses src/myproject/__init__.py,
this should be 'src/myproject/_version.py'. This file should be checked
in to your VCS as usual: the copy created below by 'setup.py
update_files' will include code that parses expanded VCS keywords in
generated tarballs. The 'build' and 'sdist' commands will replace it with
a copy that has just the calculated version string.
versionfile_build:
Like versionfile_source, but relative to the build directory instead of
the source directory. These will differ when your setup.py uses
'package_dir='. If you have package_dir={'myproject': 'src/myproject'},
then you will probably have versionfile_build='myproject/_version.py' and
versionfile_source='src/myproject/_version.py'.
tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all
VCS tags. If your tags look like 'myproject-1.2.0', then you
should use tag_prefix='myproject-'. If you use unprefixed tags
like '1.2.0', this should be an empty string.
parentdir_prefix: a string, frequently the same as tag_prefix, which
appears at the start of all unpacked tarball filenames. If
your tarball unpacks into 'myproject-1.2.0', this should
be 'myproject-'.
To use it:
1: include this file in the top level of your project
2: make the following changes to the top of your setup.py:
import versioneer
versioneer.versionfile_source = 'src/myproject/_version.py'
versioneer.versionfile_build = 'myproject/_version.py'
versioneer.tag_prefix = '' # tags are like 1.2.0
versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0'
3: add the following arguments to the setup() call in your setup.py:
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
4: run 'setup.py update_files', which will create _version.py, and will
append the following to your __init__.py:
from _version import __version__
5: modify your MANIFEST.in to include versioneer.py
6: add both versioneer.py and the generated _version.py to your VCS
"""
import os, sys, re
from distutils.core import Command
from distutils.command.sdist import sdist as _sdist
from distutils.command.build import build as _build
versionfile_source = None
versionfile_build = None
tag_prefix = None
parentdir_prefix = None
VCS = "git"
IN_LONG_VERSION_PY = False
LONG_VERSION_PY = '''
IN_LONG_VERSION_PY = True
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (build by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.7+ (https://github.com/warner/python-versioneer)
# these strings will be replaced by git during git-archive
git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %%s" %% args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %%s (error)" %% args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%%s', no digits" %% ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %%d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %%s" %% ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %%s" %% r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %%s" %% root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %%
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
tag_prefix = "%(TAG_PREFIX)s"
parentdir_prefix = "%(PARENTDIR_PREFIX)s"
versionfile_source = "%(VERSIONFILE_SOURCE)s"
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
variables = { "refnames": git_refnames, "full": git_full }
ver = versions_from_expanded_variables(variables, tag_prefix, verbose)
if not ver:
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if not ver:
ver = versions_from_parentdir(parentdir_prefix, versionfile_source,
verbose)
if not ver:
ver = default
return ver
'''
import subprocess
import sys
def run_command(args, cwd=None, verbose=False):
try:
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd)
except EnvironmentError:
e = sys.exc_info()[1]
if verbose:
print("unable to run %s" % args[0])
print(e)
return None
stdout = p.communicate()[0].strip()
if sys.version >= '3':
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % args[0])
return None
return stdout
import sys
import re
import os.path
def get_expanded_variables(versionfile_source):
# the code embedded in _version.py can just fetch the value of these
# variables. When used from setup.py, we don't want to import
# _version.py, so we do it with a regexp instead. This function is not
# used from _version.py.
variables = {}
try:
for line in open(versionfile_source,"r").readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
variables["full"] = mo.group(1)
except EnvironmentError:
pass
return variables
def versions_from_expanded_variables(variables, tag_prefix, verbose=False):
refnames = variables["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("variables are unexpanded, not using")
return {} # unexpanded, so not in an unpacked git-archive tarball
refs = set([r.strip() for r in refnames.strip("()").split(",")])
for ref in list(refs):
if not re.search(r'\d', ref):
if verbose:
print("discarding '%s', no digits" % ref)
refs.discard(ref)
# Assume all version tags have a digit. git's %d expansion
# behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us
# distinguish between branches and tags. By ignoring refnames
# without digits, we filter out many common branch names like
# "release" and "stabilization", as well as "HEAD" and "master".
if verbose:
print("remaining refs: %s" % ",".join(sorted(refs)))
for ref in sorted(refs):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return { "version": r,
"full": variables["full"].strip() }
# no suitable tags, so we use the full revision id
if verbose:
print("no suitable tags, using full revision id")
return { "version": variables["full"].strip(),
"full": variables["full"].strip() }
def versions_from_vcs(tag_prefix, versionfile_source, verbose=False):
# this runs 'git' from the root of the source tree. That either means
# someone ran a setup.py command (and this code is in versioneer.py, so
# IN_LONG_VERSION_PY=False, thus the containing directory is the root of
# the source tree), or someone ran a project-specific entry point (and
# this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the
# containing directory is somewhere deeper in the source tree). This only
# gets called if the git-archive 'subst' variables were *not* expanded,
# and _version.py hasn't already been rewritten with a short version
# string, meaning we're inside a checked out source tree.
try:
here = os.path.abspath(__file__)
except NameError:
# some py2exe/bbfreeze/non-CPython implementations don't do __file__
return {} # not always correct
# versionfile_source is the relative path from the top of the source tree
# (where the .git directory might live) to this file. Invert this to find
# the root from __file__.
root = here
if IN_LONG_VERSION_PY:
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
root = os.path.dirname(here)
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
return {}
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"],
cwd=root)
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False):
if IN_LONG_VERSION_PY:
# We're running from _version.py. If it's from a source tree
# (execute-in-place), we can work upwards to find the root of the
# tree, and then check the parent directory for a version string. If
# it's in an installed application, there's no hope.
try:
here = os.path.abspath(__file__)
except NameError:
# py2exe/bbfreeze/non-CPython don't have __file__
return {} # without __file__, we have no hope
# versionfile_source is the relative path from the top of the source
# tree to _version.py. Invert this to find the root from __file__.
root = here
for i in range(len(versionfile_source.split("/"))):
root = os.path.dirname(root)
else:
# we're running from versioneer.py, which means we're running from
# the setup.py in a source tree. sys.argv[0] is setup.py in the root.
here = os.path.abspath(sys.argv[0])
root = os.path.dirname(here)
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" %
(root, dirname, parentdir_prefix))
return None
return {"version": dirname[len(parentdir_prefix):], "full": ""}
import sys
def do_vcs_install(versionfile_source, ipy):
GIT = "git"
if sys.platform == "win32":
GIT = "git.cmd"
run_command([GIT, "add", "versioneer.py"])
run_command([GIT, "add", versionfile_source])
run_command([GIT, "add", ipy])
present = False
try:
f = open(".gitattributes", "r")
for line in f.readlines():
if line.strip().startswith(versionfile_source):
if "export-subst" in line.strip().split()[1:]:
present = True
f.close()
except EnvironmentError:
pass
if not present:
f = open(".gitattributes", "a+")
f.write("%s export-subst\n" % versionfile_source)
f.close()
run_command([GIT, "add", ".gitattributes"])
SHORT_VERSION_PY = """
# This file was generated by 'versioneer.py' (0.7+) from
# revision-control system data, or from the parent directory name of an
# unpacked source archive. Distribution tarballs contain a pre-generated copy
# of this file.
version_version = '%(version)s'
version_full = '%(full)s'
def get_versions(default={}, verbose=False):
return {'version': version_version, 'full': version_full}
"""
DEFAULT = {"version": "unknown", "full": "unknown"}
def versions_from_file(filename):
versions = {}
try:
f = open(filename)
except EnvironmentError:
return versions
for line in f.readlines():
mo = re.match("version_version = '([^']+)'", line)
if mo:
versions["version"] = mo.group(1)
mo = re.match("version_full = '([^']+)'", line)
if mo:
versions["full"] = mo.group(1)
return versions
def write_to_version_file(filename, versions):
f = open(filename, "w")
f.write(SHORT_VERSION_PY % versions)
f.close()
print("set %s to '%s'" % (filename, versions["version"]))
def get_best_versions(versionfile, tag_prefix, parentdir_prefix,
default=DEFAULT, verbose=False):
# returns dict with two keys: 'version' and 'full'
#
# extract version from first of _version.py, 'git describe', parentdir.
# This is meant to work for developers using a source checkout, for users
# of a tarball created by 'setup.py sdist', and for users of a
# tarball/zipball created by 'git archive' or github's download-from-tag
# feature.
variables = get_expanded_variables(versionfile_source)
if variables:
ver = versions_from_expanded_variables(variables, tag_prefix)
if ver:
if verbose: print("got version from expanded variable %s" % ver)
return ver
ver = versions_from_file(versionfile)
if ver:
if verbose: print("got version from file %s %s" % (versionfile, ver))
return ver
ver = versions_from_vcs(tag_prefix, versionfile_source, verbose)
if ver:
if verbose: print("got version from git %s" % ver)
return ver
ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose)
if ver:
if verbose: print("got version from parentdir %s" % ver)
return ver
if verbose: print("got version from default %s" % ver)
return default
def get_versions(default=DEFAULT, verbose=False):
assert versionfile_source is not None, "please set versioneer.versionfile_source"
assert tag_prefix is not None, "please set versioneer.tag_prefix"
assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix"
return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix,
default=default, verbose=verbose)
def get_version(verbose=False):
return get_versions(verbose=verbose)["version"]
class cmd_version(Command):
description = "report generated version string"
user_options = []
boolean_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
ver = get_version(verbose=True)
print("Version is currently: %s" % ver)
class cmd_build(_build):
def run(self):
versions = get_versions(verbose=True)
_build.run(self)
# now locate _version.py in the new build/ directory and replace it
# with an updated value
target_versionfile = os.path.join(self.build_lib, versionfile_build)
print("UPDATING %s" % target_versionfile)
os.unlink(target_versionfile)
f = open(target_versionfile, "w")
f.write(SHORT_VERSION_PY % versions)
f.close()
class cmd_sdist(_sdist):
def run(self):
versions = get_versions(verbose=True)
self._versioneer_generated_versions = versions
# unless we update this, the command will keep using the old version
self.distribution.metadata.version = versions["version"]
return _sdist.run(self)
def make_release_tree(self, base_dir, files):
_sdist.make_release_tree(self, base_dir, files)
# now locate _version.py in the new base_dir directory (remembering
# that it may be a hardlink) and replace it with an updated value
target_versionfile = os.path.join(base_dir, versionfile_source)
print("UPDATING %s" % target_versionfile)
os.unlink(target_versionfile)
f = open(target_versionfile, "w")
f.write(SHORT_VERSION_PY % self._versioneer_generated_versions)
f.close()
INIT_PY_SNIPPET = """
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
"""
class cmd_update_files(Command):
description = "modify __init__.py and create _version.py"
user_options = []
boolean_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py")
print(" creating %s" % versionfile_source)
f = open(versionfile_source, "w")
f.write(LONG_VERSION_PY % {"DOLLAR": "$",
"TAG_PREFIX": tag_prefix,
"PARENTDIR_PREFIX": parentdir_prefix,
"VERSIONFILE_SOURCE": versionfile_source,
})
f.close()
try:
old = open(ipy, "r").read()
except EnvironmentError:
old = ""
if INIT_PY_SNIPPET not in old:
print(" appending to %s" % ipy)
f = open(ipy, "a")
f.write(INIT_PY_SNIPPET)
f.close()
else:
print(" %s unmodified" % ipy)
do_vcs_install(versionfile_source, ipy)
def get_cmdclass():
return {'version': cmd_version,
'update_files': cmd_update_files,
'build': cmd_build,
'sdist': cmd_sdist,
}