diff --git a/.gitignore b/.gitignore
index a8eee82..88c630b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,8 @@ local_settings.py
keeper
build/*
build-stamp
-melange.egg-info
-.melange-venv
-.venv
+python_melangeclient.egg-info
+.tox
*.sqlite
*.log
tags
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..df9dd8d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst
+include melange/client/views/*.tpl
diff --git a/README b/README
deleted file mode 100644
index 0cb0cfe..0000000
--- a/README
+++ /dev/null
@@ -1,11 +0,0 @@
-
-To run unit tests:
- melange_client_dir> ./run_tests.sh melange_client.tests.unit
-
-To run functional tests:
-1. Start the melange server
-2. Update the configuration values in
- melange_client/tests/functional/tests.conf
- to point to the melange server
-3. Run the tests:
- melange_client_dir> ./run_tests.sh melange_client.tests.functional
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b5ea270
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,16 @@
+Python bindings to the OpenStack Melange API
+============================================
+
+This is a client for the Openstack Melange API. It contains a Python API
+(the ``melange.client`` module), and a command-line script (``melange``).
+
+Running the tests
+-----------------
+
+Currently the test suite requires a running melange-server running on
+http://localhost:9898.
+
+Tests are run under `tox `_. First install
+``tox`` using pip or your distribution's packages then run ``tox`` from
+the distribution directory to run the tests in isolated virtual
+environments.
diff --git a/melange/__init__.py b/melange/__init__.py
new file mode 100644
index 0000000..7d0d297
--- /dev/null
+++ b/melange/__init__.py
@@ -0,0 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import pkgutil
+
+
+__path__ = pkgutil.extend_path(__path__, __name__)
diff --git a/melange_client/__init__.py b/melange/client/__init__.py
similarity index 71%
rename from melange_client/__init__.py
rename to melange/client/__init__.py
index f077c6a..9ab7051 100644
--- a/melange_client/__init__.py
+++ b/melange/client/__init__.py
@@ -16,18 +16,15 @@
# under the License.
import gettext
-import os
+from melange.client.client import HTTPClient
+from melange.client.client import AuthorizationClient
+
+
+# NOTE(jkoelker) should this be melange.client? Are translations going
+# to be separate?
gettext.install('melange', unicode=1)
-def melange_root_path():
- return os.path.dirname(__file__)
-
-
-def melange_bin_path(filename="."):
- return os.path.join(melange_root_path(), "..", "bin", filename)
-
-
-def melange_etc_path(filename="."):
- return os.path.join(melange_root_path(), "..", "etc", "melange", filename)
+__all__ = [HTTPClient,
+ AuthorizationClient]
diff --git a/bin/melange b/melange/client/cli.py
old mode 100755
new mode 100644
similarity index 87%
rename from bin/melange
rename to melange/client/cli.py
index 653b8e0..8585b1a
--- a/bin/melange
+++ b/melange/client/cli.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
@@ -29,22 +28,11 @@ from os import environ as env
import sys
import yaml
-# If ../melange_client/__init__.py exists, add ../ to Python search path, so
-# it will override what happens to be installed in /usr/(local/)lib/python...
-possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
- os.pardir,
- os.pardir))
-if os.path.exists(os.path.join(possible_topdir,
- 'melange_client',
- '__init__.py')):
- sys.path.insert(0, possible_topdir)
-
-import melange_client
-from melange_client import client as base_client
-from melange_client import exception
-from melange_client import inspector
-from melange_client import ipam_client
-from melange_client import template
+from melange.client import client as base_client
+from melange.client import exception
+from melange.client import inspector
+from melange.client import ipam_client
+from melange.client import template
def create_options(parser):
@@ -164,7 +152,10 @@ def auth_client(options):
def view(data, template_name):
data = data or {}
try:
- view_path = os.path.join(melange_client.melange_root_path(), 'views')
+ # TODO(jkoelker) Templates should be using the PEP302 get_data api
+ melange_client_file = sys.modules['melange.client'].__file__
+ melange_path = os.path.dirname(melange_client_file)
+ view_path = os.path.join(melange_path, 'views')
return template.template(template_name,
template_lookup=[view_path], **data)
except exception.TemplateNotFoundError:
@@ -179,17 +170,19 @@ def args_to_dict(args):
"of the form of field=value")
-def main():
+def main(script_name=None, argv=None):
+ if argv is None:
+ argv = sys.argv[1:]
+
+ if script_name is None:
+ script_name = os.path.basename(sys.argv[0])
+
oparser = optparse.OptionParser(version='%%prog 0.1',
usage=usage())
create_options(oparser)
- (options, args) = parse_options(oparser, sys.argv[1:])
+ (options, args) = parse_options(oparser, argv)
- script_name = os.path.basename(sys.argv[0])
category = args.pop(0)
- http_client = base_client.HTTPClient(options.host,
- options.port,
- options.timeout)
factory = ipam_client.Factory(options.host,
options.port,
@@ -246,7 +239,3 @@ def main():
else:
print _("Command failed, please check log for more info")
sys.exit(2)
-
-
-if __name__ == '__main__':
- main()
diff --git a/melange_client/client.py b/melange/client/client.py
similarity index 94%
rename from melange_client/client.py
rename to melange/client/client.py
index d011422..da35c80 100644
--- a/melange_client/client.py
+++ b/melange/client/client.py
@@ -15,14 +15,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+
import httplib
import httplib2
-import json
import socket
import urllib
import urlparse
-from melange_client import exception
+from melange.client import exception
+from melange.client import utils
class HTTPClient(object):
@@ -75,7 +76,7 @@ class AuthorizationClient(httplib2.Http):
if self.auth_token:
return self.auth_token
headers = {'content-type': 'application/json'}
- request_body = json.dumps({"passwordCredentials":
+ request_body = utils.dumps({"passwordCredentials":
{"username": self.username,
'password': self.access_key}})
res, body = self.request(self.url, "POST", headers=headers,
@@ -83,4 +84,4 @@ class AuthorizationClient(httplib2.Http):
if int(res.status) >= 400:
raise Exception(_("Error occured while retrieving token : %s")
% body)
- return json.loads(body)['auth']['token']['id']
+ return utils.loads(body)['auth']['token']['id']
diff --git a/melange_client/exception.py b/melange/client/exception.py
similarity index 90%
rename from melange_client/exception.py
rename to melange/client/exception.py
index c16d303..564c451 100644
--- a/melange_client/exception.py
+++ b/melange/client/exception.py
@@ -16,10 +16,10 @@
# License for the specific language governing permissions and limitations
# under the License.
-from openstack.common import exception as openstack_exception
-
-ClientConnectionError = openstack_exception.ClientConnectionError
+class ClientConnectionError(Exception):
+ """Error resulting from a client connecting to a server"""
+ pass
class MelangeClientError(Exception):
diff --git a/melange_client/inspector.py b/melange/client/inspector.py
similarity index 100%
rename from melange_client/inspector.py
rename to melange/client/inspector.py
diff --git a/melange_client/ipam_client.py b/melange/client/ipam_client.py
similarity index 98%
rename from melange_client/ipam_client.py
rename to melange/client/ipam_client.py
index 0c6c0e9..9d75cfb 100644
--- a/melange_client/ipam_client.py
+++ b/melange/client/ipam_client.py
@@ -15,12 +15,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
import sys
import urlparse
-from melange_client import client
-from melange_client import utils
+from melange.client import client
+from melange.client import utils
class Factory(object):
@@ -71,12 +70,12 @@ class Resource(object):
def create(self, **kwargs):
return self.request("POST",
self.path,
- body=json.dumps({self.name: kwargs}))
+ body=utils.dumps({self.name: kwargs}))
def update(self, id, **kwargs):
return self.request("PUT",
self._member_path(id),
- body=json.dumps(
+ body=utils.dumps(
{self.name: utils.remove_nones(kwargs)}))
def all(self, **params):
@@ -99,7 +98,7 @@ class Resource(object):
kwargs['headers']['X-AUTH-TOKEN'] = self.auth_client.get_token()
result = self.client.do_request(method, path, **kwargs).read()
if result:
- return json.loads(result)
+ return utils.loads(result)
class BaseClient(object):
diff --git a/melange_client/template.py b/melange/client/template.py
similarity index 99%
rename from melange_client/template.py
rename to melange/client/template.py
index 19dcc63..29f0c68 100644
--- a/melange_client/template.py
+++ b/melange/client/template.py
@@ -41,12 +41,9 @@ import cgi
import re
import os
import functools
-import time
import tokenize
-import mimetypes
-import datetime
-from melange_client import exception
+from melange.client import exception
TEMPLATES = {}
DEBUG = False
diff --git a/melange_client/tests/__init__.py b/melange/client/tests/__init__.py
similarity index 97%
rename from melange_client/tests/__init__.py
rename to melange/client/tests/__init__.py
index a8c6bda..2889dd8 100644
--- a/melange_client/tests/__init__.py
+++ b/melange/client/tests/__init__.py
@@ -18,6 +18,7 @@
import unittest
+# TODO(jkoelker) Convert this to mock
import mox
diff --git a/melange/client/tests/functional/__init__.py b/melange/client/tests/functional/__init__.py
new file mode 100644
index 0000000..b601379
--- /dev/null
+++ b/melange/client/tests/functional/__init__.py
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import ConfigParser
+import cStringIO
+import os
+import sys
+
+from melange.client import cli
+
+
+def run(command, **kwargs):
+ config = ConfigParser.ConfigParser()
+ functional_path = os.path.dirname(os.path.realpath(__file__))
+ config.read(os.path.join(functional_path, "tests.conf"))
+
+ stdout = sys.stdout
+ stderr = sys.stderr
+ exit = sys.exit
+
+ mystdout = cStringIO.StringIO()
+ mystderr = cStringIO.StringIO()
+ exitcode = {'code': 0}
+
+ def myexit(code):
+ exitcode['code'] = code
+
+ sys.stdout = mystdout
+ sys.stderr = mystderr
+ sys.exit = myexit
+
+ argv = ['--host=%s' % config.get('DEFAULT', 'server_name'),
+ '--port=%s' % config.get('DEFAULT', 'server_port'),
+ '-v']
+ argv.extend(command.strip().split(' '))
+
+ cli.main(script_name='melange', argv=argv)
+
+ sys.stdout = stdout
+ sys.stderr = stderr
+ sys.exit = exit
+
+ return {'exitcode': exitcode['code'],
+ 'out': mystdout.getvalue(),
+ 'err': mystderr.getvalue()}
diff --git a/melange_client/tests/functional/factory.py b/melange/client/tests/functional/factory.py
similarity index 98%
rename from melange_client/tests/functional/factory.py
rename to melange/client/tests/functional/factory.py
index 9b2103f..d4a8dd0 100644
--- a/melange_client/tests/functional/factory.py
+++ b/melange/client/tests/functional/factory.py
@@ -18,7 +18,7 @@
import uuid
import yaml
-from melange_client.tests import functional
+from melange.client.tests import functional
def create_policy(tenant_id="123"):
diff --git a/melange_client/tests/functional/template_test_helper.py b/melange/client/tests/functional/template_test_helper.py
similarity index 100%
rename from melange_client/tests/functional/template_test_helper.py
rename to melange/client/tests/functional/template_test_helper.py
diff --git a/melange_client/tests/functional/test_cli.py b/melange/client/tests/functional/test_cli.py
similarity index 98%
rename from melange_client/tests/functional/test_cli.py
rename to melange/client/tests/functional/test_cli.py
index 592bd78..59cc9c0 100644
--- a/melange_client/tests/functional/test_cli.py
+++ b/melange/client/tests/functional/test_cli.py
@@ -15,15 +15,14 @@
# License for the specific language governing permissions and limitations
# under the License.
-import re
import uuid
import yaml
-from melange_client import tests
-from melange_client import utils
-from melange_client.tests import functional
-from melange_client.tests.functional import template_test_helper
-from melange_client.tests.functional import factory
+from melange.client import tests
+from melange.client import utils
+from melange.client.tests import functional
+from melange.client.tests.functional import template_test_helper
+from melange.client.tests.functional import factory
class TestBaseCLI(tests.BaseTest, template_test_helper.TemplateTestHelper):
@@ -109,9 +108,8 @@ class TestIpBlockCLI(TestBaseCLI):
sorted(factory.model('ip_blocks', list_res)))
def test_list_without_tenant_id_should_error_out(self):
- self.assertRaises(RuntimeError,
- functional.run,
- "ip_block list")
+ err_res = functional.run("ip_block list")
+ self.assertTrue(0 != err_res['exitcode'])
class TestSubnetCLI(TestBaseCLI):
diff --git a/melange_client/tests/functional/tests.conf b/melange/client/tests/functional/tests.conf
similarity index 100%
rename from melange_client/tests/functional/tests.conf
rename to melange/client/tests/functional/tests.conf
diff --git a/melange_client/tests/unit/__init__.py b/melange/client/tests/unit/__init__.py
similarity index 100%
rename from melange_client/tests/unit/__init__.py
rename to melange/client/tests/unit/__init__.py
diff --git a/melange_client/tests/unit/helper/__init__.py b/melange/client/tests/unit/helper/__init__.py
similarity index 100%
rename from melange_client/tests/unit/helper/__init__.py
rename to melange/client/tests/unit/helper/__init__.py
diff --git a/melange_client/tests/unit/helper/test_table.py b/melange/client/tests/unit/helper/test_table.py
similarity index 94%
rename from melange_client/tests/unit/helper/test_table.py
rename to melange/client/tests/unit/helper/test_table.py
index c6c5d60..4fb6d76 100644
--- a/melange_client/tests/unit/helper/test_table.py
+++ b/melange/client/tests/unit/helper/test_table.py
@@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-from melange_client import tests
-from melange_client.views.helpers import table
+from melange.client import tests
+from melange.client.views.helpers import table
class TestTable(tests.BaseTest):
diff --git a/melange_client/tests/unit/test_client.py b/melange/client/tests/unit/test_client.py
similarity index 94%
rename from melange_client/tests/unit/test_client.py
rename to melange/client/tests/unit/test_client.py
index 4747479..59800ec 100644
--- a/melange_client/tests/unit/test_client.py
+++ b/melange/client/tests/unit/test_client.py
@@ -16,14 +16,20 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
+try:
+ import simplejson
+ json = simplejson
+except ImportError:
+ import json
+
import urlparse
import httplib2
+# TODO(jkoelker) Convert this to mock
import mox
-from melange_client import client
-from melange_client import tests
+from melange.client import client
+from melange.client import tests
class TestAuthorizationClient(tests.BaseTest):
diff --git a/melange_client/tests/unit/test_inspector.py b/melange/client/tests/unit/test_inspector.py
similarity index 97%
rename from melange_client/tests/unit/test_inspector.py
rename to melange/client/tests/unit/test_inspector.py
index 42ec675..973c9e9 100644
--- a/melange_client/tests/unit/test_inspector.py
+++ b/melange/client/tests/unit/test_inspector.py
@@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-from melange_client import tests
-from melange_client import inspector
+from melange.client import tests
+from melange.client import inspector
class TestMethodInspector(tests.BaseTest):
diff --git a/melange_client/tests/unit/test_ipam_client.py b/melange/client/tests/unit/test_ipam_client.py
similarity index 94%
rename from melange_client/tests/unit/test_ipam_client.py
rename to melange/client/tests/unit/test_ipam_client.py
index 09920cf..3e1f1e0 100644
--- a/melange_client/tests/unit/test_ipam_client.py
+++ b/melange/client/tests/unit/test_ipam_client.py
@@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-from melange_client import ipam_client
-from melange_client import tests
+from melange.client import ipam_client
+from melange.client import tests
class TestFactory(tests.BaseTest):
diff --git a/melange_client/tests/unit/test_utils.py b/melange/client/tests/unit/test_utils.py
similarity index 95%
rename from melange_client/tests/unit/test_utils.py
rename to melange/client/tests/unit/test_utils.py
index af3292c..610fe98 100644
--- a/melange_client/tests/unit/test_utils.py
+++ b/melange/client/tests/unit/test_utils.py
@@ -15,8 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-from melange_client import tests
-from melange_client import utils
+from melange.client import tests
+from melange.client import utils
class TestUtils(tests.BaseTest):
diff --git a/melange_client/tests/functional/__init__.py b/melange/client/utils.py
similarity index 53%
rename from melange_client/tests/functional/__init__.py
rename to melange/client/utils.py
index 20ececc..9864b0a 100644
--- a/melange_client/tests/functional/__init__.py
+++ b/melange/client/utils.py
@@ -15,20 +15,29 @@
# License for the specific language governing permissions and limitations
# under the License.
-import ConfigParser
-import os
+import re
-import melange_client
-from melange_client import utils
+try:
+ # For Python < 2.6 or people using a newer version of simplejson
+ import simplejson
+ json = simplejson
+except ImportError:
+ # For Python >= 2.6
+ import json
-def run(command, **kwargs):
- config = ConfigParser.ConfigParser()
- config.read(os.path.join(melange_client.melange_root_path(),
- "tests/functional/tests.conf"))
- full_command = "{0} --host={1} --port={2} {3} -v ".format(
- melange_client.melange_bin_path('melange'),
- config.get('DEFAULT', 'server_name'),
- config.get('DEFAULT', 'server_port'),
- command)
- return utils.execute(full_command, **kwargs)
+def loads(s):
+ return json.loads(s)
+
+
+def dumps(o):
+ return json.dumps(o)
+
+
+def camelize(string):
+ return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string)
+
+
+def remove_nones(hash):
+ return dict((key, value)
+ for key, value in hash.iteritems() if value is not None)
diff --git a/melange_client/views/__init__.py b/melange/client/views/__init__.py
similarity index 100%
rename from melange_client/views/__init__.py
rename to melange/client/views/__init__.py
diff --git a/melange_client/views/helpers/__init__.py b/melange/client/views/helpers/__init__.py
similarity index 100%
rename from melange_client/views/helpers/__init__.py
rename to melange/client/views/helpers/__init__.py
diff --git a/melange_client/views/helpers/table.py b/melange/client/views/helpers/table.py
similarity index 100%
rename from melange_client/views/helpers/table.py
rename to melange/client/views/helpers/table.py
diff --git a/melange_client/views/ip_route_list.tpl b/melange/client/views/ip_route_list.tpl
similarity index 80%
rename from melange_client/views/ip_route_list.tpl
rename to melange/client/views/ip_route_list.tpl
index 27ebbf1..80584bf 100644
--- a/melange_client/views/ip_route_list.tpl
+++ b/melange/client/views/ip_route_list.tpl
@@ -1,4 +1,4 @@
-%from melange_client.views.helpers import table
+%from melange.client.views.helpers import table
{{table.row_view(table.padded_keys(ip_routes).iteritems())}}
%for route in ip_routes:
{{table.row_view(map(lambda (key,value): (route[key], value), table.padded_keys(ip_routes).iteritems()))}}
diff --git a/melange_client/utils.py b/melange_client/utils.py
deleted file mode 100644
index 56153b6..0000000
--- a/melange_client/utils.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 OpenStack LLC.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import re
-import os
-import subprocess
-
-import melange_client
-
-
-def camelize(string):
- return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string)
-
-
-def remove_nones(hash):
- return dict((key, value)
- for key, value in hash.iteritems() if value is not None)
-
-
-def execute(cmd, raise_error=True):
- """Executes a command in a subprocess.
- Returns a tuple of (exitcode, out, err), where out is the string output
- from stdout and err is the string output from stderr when
- executing the command.
-
- :param cmd: Command string to execute
- :param raise_error: If returncode is not 0 (success), then
- raise a RuntimeError? Default: True)
-
- """
-
- env = os.environ.copy()
-
- # Make sure that we use the programs in the
- # current source directory's bin/ directory.
- env['PATH'] = melange_client.melange_bin_path() + ':' + env['PATH']
- process = subprocess.Popen(cmd,
- shell=True,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- env=env)
- (out, err) = process.communicate()
- exitcode = process.returncode
- if process.returncode != 0 and raise_error:
- msg = "Command %(cmd)s did not succeed. Returned an exit "\
- "code of %(exitcode)d."\
- "\n\nSTDOUT: %(out)s"\
- "\n\nSTDERR: %(err)s" % locals()
- raise RuntimeError(msg)
- return {'exitcode': exitcode, 'out': out, 'err': err}
diff --git a/run_tests.py b/run_tests.py
deleted file mode 100644
index f90c44e..0000000
--- a/run_tests.py
+++ /dev/null
@@ -1,367 +0,0 @@
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Colorizer Code is borrowed from Twisted:
-# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Unittest runner for Nova.
-
-To run all tests
- python run_tests.py
-
-To run a single test:
- python run_tests.py test_compute:ComputeTestCase.test_run_terminate
-
-To run a single test module:
- python run_tests.py test_compute
-
- or
-
- python run_tests.py api.test_wsgi
-
-"""
-
-import gettext
-import heapq
-import logging
-import os
-import unittest
-import sys
-import time
-
-gettext.install('melange', unicode=1)
-
-from nose import config
-from nose import core
-from nose import result
-
-
-class _AnsiColorizer(object):
- """
- A colorizer is an object that loosely wraps around a stream, allowing
- callers to write text to the stream in a particular color.
-
- Colorizer classes must implement C{supported()} and C{write(text, color)}.
- """
- _colors = dict(black=30, red=31, green=32, yellow=33,
- blue=34, magenta=35, cyan=36, white=37)
-
- def __init__(self, stream):
- self.stream = stream
-
- def supported(cls, stream=sys.stdout):
- """
- A class method that returns True if the current platform supports
- coloring terminal output using this method. Returns False otherwise.
- """
- if not stream.isatty():
- return False # auto color only on TTYs
- try:
- import curses
- except ImportError:
- return False
- else:
- try:
- try:
- return curses.tigetnum("colors") > 2
- except curses.error:
- curses.setupterm()
- return curses.tigetnum("colors") > 2
- except:
- raise
- # guess false in case of error
- return False
- supported = classmethod(supported)
-
- def write(self, text, color):
- """
- Write the given text to the stream in the given color.
-
- @param text: Text to be written to the stream.
-
- @param color: A string label for a color. e.g. 'red', 'white'.
- """
- color = self._colors[color]
- self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
-
-
-class _Win32Colorizer(object):
- """
- See _AnsiColorizer docstring.
- """
- def __init__(self, stream):
- from win32console import GetStdHandle, STD_OUT_HANDLE, \
- FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
- FOREGROUND_INTENSITY
- red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
- FOREGROUND_BLUE, FOREGROUND_INTENSITY)
- self.stream = stream
- self.screenBuffer = GetStdHandle(STD_OUT_HANDLE)
- self._colors = {
- 'normal': red | green | blue,
- 'red': red | bold,
- 'green': green | bold,
- 'blue': blue | bold,
- 'yellow': red | green | bold,
- 'magenta': red | blue | bold,
- 'cyan': green | blue | bold,
- 'white': red | green | blue | bold
- }
-
- def supported(cls, stream=sys.stdout):
- try:
- import win32console
- screenBuffer = win32console.GetStdHandle(
- win32console.STD_OUT_HANDLE)
- except ImportError:
- return False
- import pywintypes
- try:
- screenBuffer.SetConsoleTextAttribute(
- win32console.FOREGROUND_RED |
- win32console.FOREGROUND_GREEN |
- win32console.FOREGROUND_BLUE)
- except pywintypes.error:
- return False
- else:
- return True
- supported = classmethod(supported)
-
- def write(self, text, color):
- color = self._colors[color]
- self.screenBuffer.SetConsoleTextAttribute(color)
- self.stream.write(text)
- self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
-
-
-class _NullColorizer(object):
- """
- See _AnsiColorizer docstring.
- """
- def __init__(self, stream):
- self.stream = stream
-
- def supported(cls, stream=sys.stdout):
- return True
- supported = classmethod(supported)
-
- def write(self, text, color):
- self.stream.write(text)
-
-
-def get_elapsed_time_color(elapsed_time):
- if elapsed_time > 1.0:
- return 'red'
- elif elapsed_time > 0.25:
- return 'yellow'
- else:
- return 'green'
-
-
-class MelangeTestResult(result.TextTestResult):
- def __init__(self, *args, **kw):
- self.show_elapsed = kw.pop('show_elapsed')
- result.TextTestResult.__init__(self, *args, **kw)
- self.num_slow_tests = 5
- self.slow_tests = [] # this is a fixed-sized heap
- self._last_case = None
- self.colorizer = None
- # NOTE(vish): reset stdout for the terminal check
- stdout = sys.stdout
- sys.stdout = sys.__stdout__
- for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
- if colorizer.supported():
- self.colorizer = colorizer(self.stream)
- break
- sys.stdout = stdout
-
- # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate
- # error results in it failing to be initialized later. Otherwise,
- # _handleElapsedTime will fail, causing the wrong error message to
- # be outputted.
- self.start_time = time.time()
-
- def getDescription(self, test):
- return str(test)
-
- def _handleElapsedTime(self, test):
- self.elapsed_time = time.time() - self.start_time
- item = (self.elapsed_time, test)
- # Record only the n-slowest tests using heap
- if len(self.slow_tests) >= self.num_slow_tests:
- heapq.heappushpop(self.slow_tests, item)
- else:
- heapq.heappush(self.slow_tests, item)
-
- def _writeElapsedTime(self, test):
- color = get_elapsed_time_color(self.elapsed_time)
- self.colorizer.write(" %.2f" % self.elapsed_time, color)
-
- def _writeResult(self, test, long_result, color, short_result, success):
- if self.showAll:
- self.colorizer.write(long_result, color)
- if self.show_elapsed and success:
- self._writeElapsedTime(test)
- self.stream.writeln()
- elif self.dots:
- self.stream.write(short_result)
- self.stream.flush()
-
- # NOTE(vish): copied from unittest with edit to add color
- def addSuccess(self, test):
- unittest.TestResult.addSuccess(self, test)
- self._handleElapsedTime(test)
- self._writeResult(test, 'OK', 'green', '.', True)
-
- # NOTE(vish): copied from unittest with edit to add color
- def addFailure(self, test, err):
- unittest.TestResult.addFailure(self, test, err)
- self._handleElapsedTime(test)
- self._writeResult(test, 'FAIL', 'red', 'F', False)
-
- # NOTE(vish): copied from nose with edit to add color
- def addError(self, test, err):
- """Overrides normal addError to add support for
- errorClasses. If the exception is a registered class, the
- error will be added to the list for that class, not errors.
- """
- self._handleElapsedTime(test)
- stream = getattr(self, 'stream', None)
- ec, ev, tb = err
- try:
- exc_info = self._exc_info_to_string(err, test)
- except TypeError:
- # 2.3 compat
- exc_info = self._exc_info_to_string(err)
- for cls, (storage, label, isfail) in self.errorClasses.items():
- if result.isclass(ec) and issubclass(ec, cls):
- if isfail:
- test.passed = False
- storage.append((test, exc_info))
- # Might get patched into a streamless result
- if stream is not None:
- if self.showAll:
- message = [label]
- detail = result._exception_detail(err[1])
- if detail:
- message.append(detail)
- stream.writeln(": ".join(message))
- elif self.dots:
- stream.write(label[:1])
- return
- self.errors.append((test, exc_info))
- test.passed = False
- if stream is not None:
- self._writeResult(test, 'ERROR', 'red', 'E', False)
-
- def startTest(self, test):
- unittest.TestResult.startTest(self, test)
- self.start_time = time.time()
- current_case = test.test.__class__.__name__
-
- if self.showAll:
- if current_case != self._last_case:
- self.stream.writeln(current_case)
- self._last_case = current_case
-
- self.stream.write(
- ' %s' % str(test.test._testMethodName).ljust(60))
- self.stream.flush()
-
-
-class MelangeTestRunner(core.TextTestRunner):
- def __init__(self, *args, **kwargs):
- self.show_elapsed = kwargs.pop('show_elapsed')
- core.TextTestRunner.__init__(self, *args, **kwargs)
-
- def _makeResult(self):
- return MelangeTestResult(self.stream,
- self.descriptions,
- self.verbosity,
- self.config,
- show_elapsed=self.show_elapsed)
-
- def _writeSlowTests(self, result_):
- # Pare out 'fast' tests
- slow_tests = [item for item in result_.slow_tests
- if get_elapsed_time_color(item[0]) != 'green']
- if slow_tests:
- slow_total_time = sum(item[0] for item in slow_tests)
- self.stream.writeln("Slowest %i tests took %.2f secs:"
- % (len(slow_tests), slow_total_time))
- for elapsed_time, test in sorted(slow_tests, reverse=True):
- time_str = "%.2f" % elapsed_time
- self.stream.writeln(" %s %s" % (time_str.ljust(10), test))
-
- def run(self, test):
- result_ = core.TextTestRunner.run(self, test)
- if self.show_elapsed:
- self._writeSlowTests(result_)
- return result_
-
-
-if __name__ == '__main__':
- logger = logging.getLogger()
- hdlr = logging.StreamHandler()
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
- hdlr.setFormatter(formatter)
- logger.addHandler(hdlr)
- logger.setLevel(logging.DEBUG)
- # If any argument looks like a test name but doesn't have "melange.tests" in
- # front of it, automatically add that so we don't have to type as much
- show_elapsed = True
- argv = []
- for x in sys.argv:
- if x.startswith('test_'):
- argv.append('melange.tests.%s' % x)
- elif x.startswith('--hide-elapsed'):
- show_elapsed = False
- else:
- argv.append(x)
-
- testdir = os.path.abspath(os.path.join("melange_client", "tests"))
- c = config.Config(stream=sys.stdout,
- env=os.environ,
- verbosity=3,
- workingDir=testdir,
- plugins=core.DefaultPluginManager())
-
- runner = MelangeTestRunner(stream=c.stream,
- verbosity=c.verbosity,
- config=c,
- show_elapsed=show_elapsed)
- sys.exit(not core.run(config=c, testRunner=runner, argv=argv))
diff --git a/run_tests.sh b/run_tests.sh
deleted file mode 100755
index 26a5d18..0000000
--- a/run_tests.sh
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-function usage {
- echo "Usage: $0 [OPTION]..."
- echo "Run Melange's test suite(s)"
- echo ""
- echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
- echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
- echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)."
- echo " -n, --no-recreate-db Don't recreate the test database."
- echo " -x, --stop Stop running tests after the first error or failure."
- echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
- echo " -p, --pep8 Just run pep8"
- echo " -P, --no-pep8 Don't run pep8"
- echo " -c, --coverage Generate coverage report"
- echo " -h, --help Print this usage message"
- echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
- echo ""
- echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
- echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
- echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
- exit
-}
-
-function process_option {
- case "$1" in
- -h|--help) usage;;
- -V|--virtual-env) always_venv=1; never_venv=0;;
- -N|--no-virtual-env) always_venv=0; never_venv=1;;
- -r|--recreate-db) recreate_db=1;;
- -n|--no-recreate-db) recreate_db=0;;
- -f|--force) force=1;;
- -p|--pep8) just_pep8=1;;
- -P|--no-pep8) no_pep8=1;;
- -c|--coverage) coverage=1;;
- -*) noseopts="$noseopts $1";;
- *) noseargs="$noseargs $1"
- esac
-}
-
-venv=.venv
-with_venv=tools/with_venv.sh
-always_venv=0
-never_venv=0
-force=0
-noseargs=
-noseopts=
-wrapper=""
-just_pep8=0
-no_pep8=0
-coverage=0
-recreate_db=1
-
-for arg in "$@"; do
- process_option $arg
-done
-
-# If enabled, tell nose to collect coverage data
-if [ $coverage -eq 1 ]; then
- noseopts="$noseopts --with-coverage --cover-package=melange"
-fi
-
-function run_tests {
- # Just run the test suites in current environment
- ${wrapper} $NOSETESTS 2> run_tests.log
- # If we get some short import error right away, print the error log directly
- RESULT=$?
- if [ "$RESULT" -ne "0" ];
- then
- ERRSIZE=`wc -l run_tests.log | awk '{print \$1}'`
- if [ "$ERRSIZE" -lt "40" ];
- then
- cat run_tests.log
- fi
- fi
- return $RESULT
-}
-
-function run_pep8 {
- echo "Running pep8 ..."
- # Opt-out files from pep8
- ignore_scripts="*.sh:*melange-debug:*clean-vlans"
- ignore_files="*eventlet-patch:*pip-requires"
- GLOBIGNORE="$ignore_scripts:$ignore_files"
- srcfiles=`find bin -type f ! -name "melange.conf*"`
- srcfiles+=" `find tools/*`"
- srcfiles+=" bin melange_client"
- # Just run PEP8 in current environment
- #
- # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the
- # following reasons:
- #
- # 1. It's needed to preserve traceback information when re-raising
- # exceptions; this is needed b/c Eventlet will clear exceptions when
- # switching contexts.
- #
- # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this
- # in Python 2 (in Python 3 `with_traceback` could be used).
- #
- # 3. Can find no corroborating evidence that this is deprecated in Python 2
- # other than what the PEP8 tool claims. It is deprecated in Python 3, so,
- # perhaps the mistake was thinking that the deprecation applied to Python 2
- # as well.
- ${wrapper} pep8 --repeat --show-pep8 --show-source \
- --ignore=E202,W602 \
- --exclude=vcsversion.py ${srcfiles}
-}
-
-NOSETESTS="python run_tests.py $noseopts $noseargs"
-
-if [ $never_venv -eq 0 ]
-then
- # Remove the virtual environment if --force used
- if [ $force -eq 1 ]; then
- echo "Cleaning virtualenv..."
- rm -rf ${venv}
- fi
- if [ -e ${venv} ]; then
- wrapper="${with_venv}"
- else
- if [ $always_venv -eq 1 ]; then
- # Automatically install the virtualenv
- python tools/install_venv.py
- wrapper="${with_venv}"
- else
- echo -e "No virtual environment found...create one? (Y/n) \c"
- read use_ve
- if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
- # Install the virtualenv and run the test suite in it
- python tools/install_venv.py
- wrapper=${with_venv}
- fi
- fi
- fi
-fi
-
-# Delete old coverage data from previous runs
-if [ $coverage -eq 1 ]; then
- ${wrapper} coverage erase
-fi
-
-if [ $just_pep8 -eq 1 ]; then
- run_pep8
- exit
-fi
-
-if [ $recreate_db -eq 1 ]; then
- rm -f tests.sqlite
-fi
-
-run_tests
-
-# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
-# not when we're running tests individually. To handle this, we need to
-# distinguish between options (noseopts), which begin with a '-', and
-# arguments (noseargs).
-if [ -z "$noseargs" ]; then
- if [ $no_pep8 -eq 0 ]; then
- run_pep8
- fi
-fi
-
-if [ $coverage -eq 1 ]; then
- echo "Generating coverage report in covhtml/"
- ${wrapper} coverage html -d covhtml -i
-fi
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8bbf905
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,15 @@
+[nosetests]
+verbosity=2
+detailed-errors=1
+with-tissue=1
+tissue-repeat=1
+tissue-show-pep8=1
+tissue-show-source=1
+tissue-inclusive=1
+tissue-color=1
+tissue-package=melange.client
+with-openstack=1
+openstack-red=0.1
+openstack-yellow=0.075
+openstack-show-elapsed=1
+openstack-color=1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..2b4ea14
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,57 @@
+# Copyright 2011 OpenStack, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+import setuptools
+
+version = "0.1"
+install_requires = ["httplib2", "pyyaml"]
+
+if sys.version_info < (2, 6):
+ install_requires.append("simplejson")
+
+classifiers = ["Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Information Technology",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ ]
+
+console_scripts = ["melange = melange.client.cli:main"]
+
+
+def read_file(file_name):
+ return open(os.path.join(os.path.dirname(__file__),
+ file_name)).read()
+
+
+setuptools.setup(name="python-melangeclient",
+ version=version,
+ description="Client library for OpenStack Melange API.",
+ long_description=read_file("README.rst"),
+ license="Apache License, Version 2.0",
+ url="https://github.com/openstack/python-melangeclient",
+ classifiers=classifiers,
+ author="Openstack Melange Team",
+ author_email="openstack@lists.launchpad.net",
+ include_package_data=True,
+ packages=setuptools.find_packages(exclude=["tests"]),
+ install_requires=install_requires,
+ entry_points = {"console_scripts": console_scripts},
+ zip_safe=False,
+)
diff --git a/tools/install_venv.py b/tools/install_venv.py
deleted file mode 100644
index 952b21c..0000000
--- a/tools/install_venv.py
+++ /dev/null
@@ -1,145 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Copyright 2010 OpenStack, LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Installation script for Melange client's development virtualenv.
-"""
-
-import os
-import subprocess
-import sys
-
-
-ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-VENV = os.path.join(ROOT, '.venv')
-PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
-PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
-
-
-def die(message, *args):
- print >> sys.stderr, message % args
- sys.exit(1)
-
-
-def check_python_version():
- if sys.version_info < (2, 6):
- die("Need Python Version >= 2.6")
-
-
-def run_command(cmd, redirect_output=True, check_exit_code=True):
- """
- Runs a command in an out-of-process shell, returning the
- output of that command. Working directory is ROOT.
- """
- if redirect_output:
- stdout = subprocess.PIPE
- else:
- stdout = None
-
- proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
- output = proc.communicate()[0]
- if check_exit_code and proc.returncode != 0:
- die('Command "%s" failed.\n%s', ' '.join(cmd), output)
- return output
-
-
-HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'],
- check_exit_code=False).strip())
-HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'],
- check_exit_code=False).strip())
-
-
-def check_dependencies():
- """Make sure virtualenv is in the path."""
-
- if not HAS_VIRTUALENV:
- print 'not found.'
- # Try installing it via easy_install...
- if HAS_EASY_INSTALL:
- print 'Installing virtualenv via easy_install...',
- if not (run_command(['which', 'easy_install']) and
- run_command(['easy_install', 'virtualenv'])):
- die('ERROR: virtualenv not found.\n\nMelange client'
- ' development requires virtualenv, please install it'
- ' using your favorite package management tool')
- print 'done.'
- print 'done.'
-
-
-def create_virtualenv(venv=VENV):
- """Creates the virtual environment and installs PIP only into the
- virtual environment
- """
- print 'Creating venv...',
- run_command(['virtualenv', '-q', '--no-site-packages', VENV])
- print 'done.'
- print 'Installing pip in virtualenv...',
- if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip():
- die("Failed to install pip.")
- print 'done.'
-
-
-def install_dependencies(venv=VENV):
- print 'Installing dependencies with pip (this can take a while)...'
- # Install greenlet by hand - just listing it in the requires file does not
- # get it in stalled in the right order
- run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv,
- 'greenlet'], redirect_output=False)
- run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r',
- PIP_REQUIRES], redirect_output=False)
-
- # Tell the virtual env how to "import melange"
- pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
- "melange.pth")
- f = open(pthfile, 'w')
- f.write("%s\n" % ROOT)
-
-
-def print_help():
- help = """
- Melange client development environment setup is complete.
-
- Melange client development uses virtualenv to track and manage Python
- dependencies while in development and testing.
-
- To activate the Melange client virtualenv for the extent of your current
- shell session you can run:
-
- $ source .venv/bin/activate
-
- Or, if you prefer, you can run commands in the virtualenv on a case by case
- basis by running:
-
- $ tools/with_venv.sh
-
- Also, make test will automatically use the virtualenv.
- """
- print help
-
-
-def main(argv):
- check_python_version()
- check_dependencies()
- create_virtualenv()
- install_dependencies()
- print_help()
-
-if __name__ == '__main__':
- main(sys.argv)
diff --git a/tools/pip-requires b/tools/pip-requires
deleted file mode 100644
index a2abbe2..0000000
--- a/tools/pip-requires
+++ /dev/null
@@ -1,10 +0,0 @@
-pep8
-pylint
-mox
-nose
-sphinx
-coverage
-nosexcover
-httplib2
-pyyaml
--e git+https://github.com/jkoelker/openstack-common.git@melange_compat#egg=openstack.common
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
deleted file mode 100755
index c8d2940..0000000
--- a/tools/with_venv.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-TOOLS=`dirname $0`
-VENV=$TOOLS/../.venv
-source $VENV/bin/activate && $@
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..579d32c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,9 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+deps= nose
+ mox
+ tissue
+ openstack.nose_plugin
+commands=nosetests []