[SEBA-450] (part 2)

Add tox testing support on additional XOS library modules:

- xos-api
- xos-kafka (has no tests)
- xos-migrate (has no tests)
- xos-synchronizer

Change-Id: I98195bc9747971d3515882d517affe058dd86ac5
diff --git a/lib/xos-api/.gitignore b/lib/xos-api/.gitignore
index f82b325..b8cf076 100644
--- a/lib/xos-api/.gitignore
+++ b/lib/xos-api/.gitignore
@@ -1,3 +1,2 @@
-build
-xosapi/chameleon
-xosapi.egg-info
+# setup.py copies this, don't commit it
+xosapi/VERSION
diff --git a/lib/xos-api/MANIFEST.in b/lib/xos-api/MANIFEST.in
new file mode 100644
index 0000000..223efb9
--- /dev/null
+++ b/lib/xos-api/MANIFEST.in
@@ -0,0 +1,2 @@
+include requirements.txt
+include xosapi/VERSION
diff --git a/lib/xos-api/requirements.txt b/lib/xos-api/requirements.txt
new file mode 100644
index 0000000..976b014
--- /dev/null
+++ b/lib/xos-api/requirements.txt
@@ -0,0 +1,9 @@
+Twisted~=16.6.0
+multistructlog~=2.1.0
+xosconfig~=2.2.6
+xosgenx~=2.2.6
+googleapis-common-protos~=1.5.6
+pykwalify~=1.6.0
+PyYAML~=3.12
+python-consul~=1.1.0
+grpcio~=1.12.0
diff --git a/lib/xos-api/setup.py b/lib/xos-api/setup.py
index 32c566f..92c7898 100644
--- a/lib/xos-api/setup.py
+++ b/lib/xos-api/setup.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,17 +12,30 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 import os
+from shutil import copyfile
+
+from setuptools import setup
 from setuptools.command.install import install
 
-try:
-    from xosutil.autoversion_setup import setup_with_auto_version as setup
-except ImportError:
-    # xosutil is not installed. Expect this to happen when we build an egg, in which case xosgenx.version will
-    # automatically have the right version.
-    from setuptools import setup
 
-from xosapi.version import __version__
+def version():
+    # Copy VERSION file of parent to module directory if not found
+    if not os.path.exists("xosapi/VERSION"):
+        copyfile("../../VERSION", "xosapi/VERSION")
+    with open("xosapi/VERSION") as f:
+        return f.read().strip()
+
+
+def parse_requirements(filename):
+    # parse a requirements.txt file, allowing for blank lines and comments
+    requirements = []
+    for line in open(filename):
+        if line and not line.startswith("#"):
+            requirements.append(line)
+    return requirements
 
 
 class InstallFixChameleonPermissions(install):
@@ -41,9 +52,11 @@
 
 setup_result = setup(
     name="xosapi",
-    version=__version__,
+    version=version(),
+    classifiers=["License :: OSI Approved :: Apache Software License"],
+    license="Apache v2",
     cmdclass={"install": InstallFixChameleonPermissions},
-    description="XOS api client",
+    description="XOS API client",
     packages=[
         "xosapi.chameleon_client",
         "xosapi.chameleon_client.protos",
@@ -51,5 +64,11 @@
         "xosapi",
         "xosapi.convenience",
     ],
+    install_requires=parse_requirements("requirements.txt"),
+    include_package_data=True,
+    package_data={
+        "xosapi.chameleon_client.protos": ["*.proto"],
+        "xosapi.chameleon_client.protoc_plugins": ["*.desc"],
+    },
     scripts=["xossh"],
 )
diff --git a/lib/xos-api/xosapi/fake_stub.py b/lib/xos-api/tests/fake_stub.py
similarity index 98%
rename from lib/xos-api/xosapi/fake_stub.py
rename to lib/xos-api/tests/fake_stub.py
index abfce00..81c3988 100644
--- a/lib/xos-api/xosapi/fake_stub.py
+++ b/lib/xos-api/tests/fake_stub.py
@@ -17,6 +17,8 @@
     Implements a simple fake grpc stub to use for unit testing.
 """
 
+from __future__ import absolute_import
+
 import functools
 
 ContentTypeMap = {}
@@ -492,9 +494,9 @@
         items = []
 
         if query.kind == FakeQuery.SYNCHRONIZER_DELETED_OBJECTS:
-            objs = self.deleted_objs.items()
+            objs = list(self.deleted_objs.items())
         else:
-            objs = self.objs.items()
+            objs = list(self.objs.items())
 
         for (k, v) in objs:
             (this_classname, id) = k.split(":")
diff --git a/lib/xos-api/tests/test_chameleon_client.py b/lib/xos-api/tests/test_chameleon_client.py
new file mode 100644
index 0000000..24b19ca
--- /dev/null
+++ b/lib/xos-api/tests/test_chameleon_client.py
@@ -0,0 +1,26 @@
+# Copyright 2019-present Open Networking Foundation
+#
+# 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 unittest
+
+import xosapi.chameleon_client.grpc_client as chameleon_client
+
+
+class TestChameleonClient(unittest.TestCase):
+
+    def test_chameleon_init(self):
+        """ test that chameleon_client can be loaded """
+
+        cclient = chameleon_client.GrpcClient("consul_endpoint", "work_dir")
+        self.assertEqual(type(cclient), chameleon_client.GrpcClient)
diff --git a/lib/xos-api/xosapi/test_config.yaml b/lib/xos-api/tests/test_config.yaml
similarity index 100%
rename from lib/xos-api/xosapi/test_config.yaml
rename to lib/xos-api/tests/test_config.yaml
diff --git a/lib/xos-api/xosapi/test_orm.py b/lib/xos-api/tests/test_orm.py
similarity index 97%
rename from lib/xos-api/xosapi/test_orm.py
rename to lib/xos-api/tests/test_orm.py
index 88bf6d4..86be969 100644
--- a/lib/xos-api/xosapi/test_orm.py
+++ b/lib/xos-api/tests/test_orm.py
@@ -13,14 +13,20 @@
 # limitations under the License.
 
 from __future__ import print_function
-import exceptions
+
 import os
 import random
 import string
 import sys
 import unittest
-from mock import patch
-from StringIO import StringIO
+
+try:  # python 3
+    from io import StringIO
+    from unittest.mock import patch
+except ImportError:  # python 2
+    from StringIO import StringIO
+    from mock import patch
+
 
 # by default, use fake stub rather than real core
 USE_FAKE_STUB = True
@@ -498,7 +504,7 @@
         with self.assertRaises(Exception) as e:
 
             self.assertEqual(
-                e.exception.message,
+                str(e.exception),
                 "Content_type does_not_exist not found in self.content_type_map",
             )
 
@@ -520,7 +526,7 @@
         self.assertTrue(tag.id > 0)
         with self.assertRaises(Exception) as e:
             self.assertEqual(
-                e.exception.message, "Object 4567 of model Site was not found"
+                str(e.exception), "Object 4567 of model Site was not found"
             )
 
     def test_generic_foreign_key_set(self):
@@ -600,7 +606,7 @@
         with self.assertRaises(Exception) as e:
             tm.intfile = None
         self.assertEqual(
-            e.exception.message,
+            str(e.exception),
             "Setting a non-foreignkey field to None is not supported",
         )
 
@@ -1013,11 +1019,11 @@
 
         with self.assertRaises(Exception) as e:
             lobjs.add(123)
-        self.assertEqual(e.exception.message, "Only ManyToMany lists are writeable")
+        self.assertEqual(str(e.exception), "Only ManyToMany lists are writeable")
 
         with self.assertRaises(Exception) as e:
             lobjs.remove(123)
-        self.assertEqual(e.exception.message, "Only ManyToMany lists are writeable")
+        self.assertEqual(str(e.exception), "Only ManyToMany lists are writeable")
 
     def test_ORMLocalObjectManager_add(self):
         orm = self.make_coreapi()
@@ -1067,9 +1073,11 @@
             try:
                 sys.argv = sys.argv[
                     :1
-                ]  # unittest does not like xos_grpc_client's command line arguments (TODO: find a cooperative approach)
+                ]
+                # unittest does not like xos_grpc_client's command line
+                # arguments (TODO: find a cooperative approach)
                 unittest.main()
-            except exceptions.SystemExit as e:
+            except SystemExit as e:
                 global exitStatus
                 exitStatus = e.code
 
diff --git a/lib/xos-api/xosapi/test_wrapper.py b/lib/xos-api/tests/test_wrapper.py
similarity index 97%
rename from lib/xos-api/xosapi/test_wrapper.py
rename to lib/xos-api/tests/test_wrapper.py
index fe4f9c1..8cb5255 100644
--- a/lib/xos-api/xosapi/test_wrapper.py
+++ b/lib/xos-api/tests/test_wrapper.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 from __future__ import print_function
-import exceptions
+
 import os
 import random
 import string
@@ -213,9 +213,11 @@
             try:
                 sys.argv = sys.argv[
                     :1
-                ]  # unittest does not like xos_grpc_client's command line arguments (TODO: find a cooperative approach)
+                ]
+                # unittest does not like xos_grpc_client's command line arguments
+                # (TODO: find a cooperative approach)
                 unittest.main()
-            except exceptions.SystemExit as e:
+            except SystemExit as e:
                 global exitStatus
                 exitStatus = e.code
 
diff --git a/lib/xos-api/tox.ini b/lib/xos-api/tox.ini
new file mode 100644
index 0000000..c2a889c
--- /dev/null
+++ b/lib/xos-api/tox.ini
@@ -0,0 +1,49 @@
+; Copyright 2017-present Open Networking Foundation
+;
+; 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.
+
+[tox]
+envlist = py27,py35,py36,py37
+skip_missing_interpreters = true
+
+[testenv]
+deps =
+  -r requirements.txt
+  nose2
+  flake8
+
+commands =
+  nose2 -c tox.ini --verbose --junit-xml
+  flake8
+
+[flake8]
+exclude =
+  .tox
+  build
+  xosapi/chameleon_client/protos
+max-line-length = 119
+
+[unittest]
+plugins=nose2.plugins.junitxml
+
+[junit-xml]
+path=nose2-results.xml
+
+[coverage]
+always-on = True
+coverage =
+  xosapi
+  tests
+coverage-report =
+  xml
+  term
diff --git a/lib/xos-api/xosapi/.gitignore b/lib/xos-api/xosapi/.gitignore
new file mode 100644
index 0000000..b8cf076
--- /dev/null
+++ b/lib/xos-api/xosapi/.gitignore
@@ -0,0 +1,2 @@
+# setup.py copies this, don't commit it
+xosapi/VERSION
diff --git a/lib/xos-api/xosapi/chameleon_client/asleep.py b/lib/xos-api/xosapi/chameleon_client/asleep.py
index 295f675..cdba793 100644
--- a/lib/xos-api/xosapi/chameleon_client/asleep.py
+++ b/lib/xos-api/xosapi/chameleon_client/asleep.py
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+from __future__ import absolute_import
 from twisted.internet import reactor
 from twisted.internet.defer import Deferred
 
@@ -25,4 +26,4 @@
     """
     d = Deferred()
     reactor.callLater(dt, lambda: d.callback(None))
-    return d
\ No newline at end of file
+    return d
diff --git a/lib/xos-api/xosapi/chameleon_client/grpc_client.py b/lib/xos-api/xosapi/chameleon_client/grpc_client.py
index a8ad69d..7d3839d 100644
--- a/lib/xos-api/xosapi/chameleon_client/grpc_client.py
+++ b/lib/xos-api/xosapi/chameleon_client/grpc_client.py
@@ -20,6 +20,7 @@
 semantics are derived from the recovered schema.
 """
 
+from __future__ import absolute_import
 import os
 import sys
 import time
@@ -33,12 +34,12 @@
 from structlog import get_logger
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
-from werkzeug.exceptions import ServiceUnavailable
+from twisted.internet.error import ConnectError
 
-from protos.schema_pb2_grpc import SchemaServiceStub
+from .protos.schema_pb2_grpc import SchemaServiceStub
 from google.protobuf.empty_pb2 import Empty
 
-from asleep import asleep
+from .asleep import asleep
 
 log = get_logger()
 
@@ -143,14 +144,14 @@
 
             return
 
-        except _Rendezvous, e:
+        except _Rendezvous as e:
             if e.code() == grpc.StatusCode.UNAVAILABLE:
                 log.info('grpc-endpoint-not-available')
             else:
                 log.exception('rendezvous error', e=e)
             yield self._backoff('not-available')
 
-        except Exception, e:
+        except Exception:
             if not self.shutting_down:
                 log.exception('cannot-connect', endpoint=_endpoint)
             yield self._backoff('unknown-error')
@@ -277,7 +278,7 @@
                       if f.endswith('_pb2.py')]:
             modname = fname[:-len('.py')]
             log.debug('test-import', modname=modname)
-            _ = __import__(modname)
+            _ = __import__(modname)  # noqa: F841
 
     @inlineCallbacks
     def invoke(self, stub, method_name, request, metadata, retry=1):
@@ -291,17 +292,17 @@
         """
 
         if not self.connected:
-            raise ServiceUnavailable()
+            raise ConnectError()
 
         try:
             method = getattr(stub(self.channel), method_name)
             response, rendezvous = method.with_call(request, metadata=metadata)
             returnValue((response, rendezvous.trailing_metadata()))
 
-        except grpc._channel._Rendezvous, e:
+        except grpc._channel._Rendezvous as e:
             code = e.code()
             if code == grpc.StatusCode.UNAVAILABLE:
-                e = ServiceUnavailable()
+                e = ConnectError()
 
                 if self.connected:
                     self.connected = False
diff --git a/lib/xos-api/xosapi/chameleon_client/protoc_plugins/gw_gen.py b/lib/xos-api/xosapi/chameleon_client/protoc_plugins/gw_gen.py
index 9cd2f6f..cafee67 100755
--- a/lib/xos-api/xosapi/chameleon_client/protoc_plugins/gw_gen.py
+++ b/lib/xos-api/xosapi/chameleon_client/protoc_plugins/gw_gen.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 #
 
+from __future__ import absolute_import
 import sys
 
 from google.protobuf.compiler import plugin_pb2 as plugin
@@ -23,7 +24,7 @@
 from jinja2 import Template
 from simplejson import dumps
 
-from xosapi.chameleon_client.protos import annotations_pb2, http_pb2
+from xosapi.chameleon_client.protos import http_pb2
 
 template = Template("""
 # Generated file; please do not edit
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2.py b/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2.py
index 5abc663..aa451e5 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2.py
@@ -18,6 +18,7 @@
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: annotations.proto
 
+from __future__ import absolute_import
 import sys
 _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
 from google.protobuf import descriptor as _descriptor
@@ -30,7 +31,7 @@
 _sym_db = _symbol_database.Default()
 
 
-import http_pb2 as http__pb2
+from . import http_pb2 as http__pb2
 from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
 
 
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2_grpc.py b/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2_grpc.py
index 972edf5..1ec9f40 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2_grpc.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/annotations_pb2_grpc.py
@@ -16,4 +16,5 @@
 #
 
 # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+from __future__ import absolute_import
 import grpc
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/http_pb2.py b/lib/xos-api/xosapi/chameleon_client/protos/http_pb2.py
index f420e61..8e359f5 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/http_pb2.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/http_pb2.py
@@ -18,6 +18,7 @@
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: http.proto
 
+from __future__ import absolute_import
 import sys
 _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
 from google.protobuf import descriptor as _descriptor
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/http_pb2_grpc.py b/lib/xos-api/xosapi/chameleon_client/protos/http_pb2_grpc.py
index 972edf5..1ec9f40 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/http_pb2_grpc.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/http_pb2_grpc.py
@@ -16,4 +16,5 @@
 #
 
 # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+from __future__ import absolute_import
 import grpc
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2.py b/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2.py
index 06909b4..0e0159a 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2.py
@@ -18,6 +18,7 @@
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: schema.proto
 
+from __future__ import absolute_import
 import sys
 _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
 from google.protobuf import descriptor as _descriptor
diff --git a/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2_grpc.py b/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2_grpc.py
index 65c9afa..06c1456 100644
--- a/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2_grpc.py
+++ b/lib/xos-api/xosapi/chameleon_client/protos/schema_pb2_grpc.py
@@ -16,10 +16,11 @@
 #
 
 # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+from __future__ import absolute_import
 import grpc
 
 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
-import schema_pb2 as schema__pb2
+from . import schema_pb2 as schema__pb2
 
 
 class SchemaServiceStub(object):
diff --git a/lib/xos-api/xosapi/convenience/addresspool.py b/lib/xos-api/xosapi/convenience/addresspool.py
index 13006c5..e83c818 100644
--- a/lib/xos-api/xosapi/convenience/addresspool.py
+++ b/lib/xos-api/xosapi/convenience/addresspool.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/controller.py b/lib/xos-api/xosapi/convenience/controller.py
index 437b54a..4e13dac 100644
--- a/lib/xos-api/xosapi/convenience/controller.py
+++ b/lib/xos-api/xosapi/convenience/controller.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/instance.py b/lib/xos-api/xosapi/convenience/instance.py
index 3941643..f373f12 100644
--- a/lib/xos-api/xosapi/convenience/instance.py
+++ b/lib/xos-api/xosapi/convenience/instance.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/network.py b/lib/xos-api/xosapi/convenience/network.py
index ffa89f7..95cb972 100644
--- a/lib/xos-api/xosapi/convenience/network.py
+++ b/lib/xos-api/xosapi/convenience/network.py
@@ -12,7 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from xosapi.orm import ORMWrapper, ORMLocalObjectManager, register_convenience_wrapper
+from __future__ import absolute_import
+
+from xosapi.orm import (ORMLocalObjectManager, ORMWrapper,
+                        register_convenience_wrapper)
 
 
 class ORMWrapperNetwork(ORMWrapper):
diff --git a/lib/xos-api/xosapi/convenience/onosapp.py b/lib/xos-api/xosapi/convenience/onosapp.py
index fd791a8..2970581 100644
--- a/lib/xos-api/xosapi/convenience/onosapp.py
+++ b/lib/xos-api/xosapi/convenience/onosapp.py
@@ -12,8 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from xosapi.orm import register_convenience_wrapper
+from __future__ import absolute_import
+
 from xosapi.convenience.serviceinstance import ORMWrapperServiceInstance
+from xosapi.orm import register_convenience_wrapper
 
 
 class ORMWrapperONOSApp(ORMWrapperServiceInstance):
diff --git a/lib/xos-api/xosapi/convenience/port.py b/lib/xos-api/xosapi/convenience/port.py
index 61e7377..2c9b9a6 100644
--- a/lib/xos-api/xosapi/convenience/port.py
+++ b/lib/xos-api/xosapi/convenience/port.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/privilege.py b/lib/xos-api/xosapi/convenience/privilege.py
index 9bdb635..0071842 100644
--- a/lib/xos-api/xosapi/convenience/privilege.py
+++ b/lib/xos-api/xosapi/convenience/privilege.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/service.py b/lib/xos-api/xosapi/convenience/service.py
index ab82908..8c9985a 100644
--- a/lib/xos-api/xosapi/convenience/service.py
+++ b/lib/xos-api/xosapi/convenience/service.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/serviceinstance.py b/lib/xos-api/xosapi/convenience/serviceinstance.py
index 33ab553..10adb16 100644
--- a/lib/xos-api/xosapi/convenience/serviceinstance.py
+++ b/lib/xos-api/xosapi/convenience/serviceinstance.py
@@ -11,10 +11,11 @@
 # limitations under the License.
 
 
-from xosapi.orm import ORMWrapper, register_convenience_wrapper
+from __future__ import absolute_import
 
-from xosconfig import Config
 from multistructlog import create_logger
+from xosapi.orm import ORMWrapper, register_convenience_wrapper
+from xosconfig import Config
 
 log = create_logger(Config().get("logging"))
 
@@ -28,11 +29,6 @@
         return attrs
 
     @property
-    def tenantattribute_dict(self):
-        log.warn("tenantattribute_dict method is deprecated")
-        return self.serviceinstanceattribute_dict
-
-    @property
     def westbound_service_instances(self):
         links = self.provided_links.all()
         instances = []
@@ -50,32 +46,6 @@
 
         return instances
 
-    def create_eastbound_instance(self):
-
-        # Already has a chain
-        if len(self.eastbound_service_instances) > 0 and not self.is_new:
-            log.debug("MODEL_POLICY: Subscriber %s is already part of a chain" % si.id)
-            return
-
-        # if it does not have a chain,
-        # Find links to the next element in the service chain
-        # and create one
-
-        links = self.owner.subscribed_dependencies.all()
-
-        for link in links:
-            si_class = link.provider_service.get_service_instance_class_name()
-            log.info(" %s creating %s" % (self.model_name, si_class))
-
-            eastbound_si_class = model_accessor.get_model_class(si_class)
-            eastbound_si = eastbound_si_class()
-            eastbound_si.owner_id = link.provider_service_id
-            eastbound_si.save()
-            link = ServiceInstanceLink(
-                provider_service_instance=eastbound_si, subscriber_service_instance=si
-            )
-            link.save()
-
     def get_westbound_service_instance_properties(self, prop_name, include_self=False):
         if include_self and hasattr(self, prop_name):
             return getattr(self, prop_name)
diff --git a/lib/xos-api/xosapi/convenience/slice.py b/lib/xos-api/xosapi/convenience/slice.py
index abdd485..0a10311 100644
--- a/lib/xos-api/xosapi/convenience/slice.py
+++ b/lib/xos-api/xosapi/convenience/slice.py
@@ -12,7 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from xosapi.orm import ORMWrapper, ORMLocalObjectManager, register_convenience_wrapper
+from __future__ import absolute_import
+
+from xosapi.orm import (ORMLocalObjectManager, ORMWrapper,
+                        register_convenience_wrapper)
 
 
 class ORMWrapperSlice(ORMWrapper):
diff --git a/lib/xos-api/xosapi/convenience/tag.py b/lib/xos-api/xosapi/convenience/tag.py
index aaeaf65..eb6490f 100644
--- a/lib/xos-api/xosapi/convenience/tag.py
+++ b/lib/xos-api/xosapi/convenience/tag.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/convenience/user.py b/lib/xos-api/xosapi/convenience/user.py
index 3205680..7dda414 100644
--- a/lib/xos-api/xosapi/convenience/user.py
+++ b/lib/xos-api/xosapi/convenience/user.py
@@ -12,7 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
+
 import hashlib
+
 from xosapi.orm import ORMWrapper, register_convenience_wrapper
 
 
diff --git a/lib/xos-api/xosapi/orm.py b/lib/xos-api/xosapi/orm.py
index 41d775f..08b73e3 100644
--- a/lib/xos-api/xosapi/orm.py
+++ b/lib/xos-api/xosapi/orm.py
@@ -12,15 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
+
+import imp
 import os
 import sys
 import threading
 import time
-import imp
 import traceback
-from xosconfig import Config
+
 from multistructlog import create_logger
+from xosconfig import Config
 
 """
 Django-like ORM layer for gRPC
@@ -43,23 +45,26 @@
 u=c.xos_orm.User.objects.get(id=1)
 """
 
-
 log = create_logger(Config().get("logging"))
 
 convenience_wrappers = {}
 
-# Find the topmost synchronizer-specific function in the call stack
+
 def get_synchronizer_function():
+    """
+    Find the topmost synchronizer-specific function in the call stack
+    """
     result = None
-    for file,line,func,stmt in traceback.extract_stack():
+    for file, line, func, stmt in traceback.extract_stack():
         if file.startswith("/opt/xos/synchronizers"):
             if not result:
-                result = "%s:%s()" % (file,func)
+                result = "%s:%s()" % (file, func)
             if not file.startswith("/opt/xos/synchronizers/new_base"):
-                result = "%s:%s()" % (file,func)
+                result = "%s:%s()" % (file, func)
                 break
     return result
 
+
 class ORMGenericContentNotFoundException(Exception):
     pass
 
@@ -106,7 +111,7 @@
     def diff(self):
         d1 = self._initial
         d2 = self._dict
-        all_field_names = self._wrapped_class.DESCRIPTOR.fields_by_name.keys()
+        all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
         diffs = []
         for k in all_field_names:
             if d1.get(k, None) != d2.get(k, None):
@@ -127,11 +132,11 @@
             that are set to default values.
         """
         if self.is_new:
-            return self._dict.keys()
-        return self.diff.keys()
+            return list(self._dict.keys())
+        return list(self.diff.keys())
 
     def has_field_changed(self, field_name):
-        return field_name in self.diff.keys()
+        return field_name in list(self.diff.keys())
 
     def get_field_diff(self, field_name):
         return self.diff.get(field_name, None)
@@ -166,7 +171,7 @@
     def gen_fkmap(self):
         fkmap = {}
 
-        all_field_names = self._wrapped_class.DESCRIPTOR.fields_by_name.keys()
+        all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
 
         for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
             if name.endswith("_id"):
@@ -349,10 +354,10 @@
         if name == "pk":
             name = "id"
 
-        if name in self._fkmap.keys():
+        if name in list(self._fkmap.keys()):
             return self.fk_resolve(name)
 
-        if name in self._reverse_fkmap.keys():
+        if name in list(self._reverse_fkmap.keys()):
             return self.reverse_fk_resolve(name)
 
         try:
@@ -368,7 +373,7 @@
         return getattr(self._wrapped_class, name, *args, **kwargs)
 
     def __setattr__(self, name, value):
-        if name in self._fkmap.keys():
+        if name in list(self._fkmap.keys()):
             self.fk_set(name, value)
         elif name in self.__dict__:
             super(ORMWrapper, self).__setattr__(name, value)
@@ -423,17 +428,28 @@
     ):
         classname = self._wrapped_class.__class__.__name__
         if self.is_new:
-            log.debug("save(): is new", classname=classname, syncstep=get_synchronizer_function())
-            new_class = self.stub.invoke(
-                "Create%s" % classname, self._wrapped_class
+            log.debug(
+                "save(): is new",
+                classname=classname,
+                syncstep=get_synchronizer_function(),
             )
+            new_class = self.stub.invoke("Create%s" % classname, self._wrapped_class)
             self._wrapped_class = new_class
             self.is_new = False
         else:
             if self.has_changed:
-              log.debug("save(): updated", classname=classname, changed_fields=self.changed_fields, syncstep=get_synchronizer_function())
+                log.debug(
+                    "save(): updated",
+                    classname=classname,
+                    changed_fields=self.changed_fields,
+                    syncstep=get_synchronizer_function(),
+                )
             else:
-              log.debug("save(): no changes", classname=classname, syncstep=get_synchronizer_function())
+                log.debug(
+                    "save(): no changes",
+                    classname=classname,
+                    syncstep=get_synchronizer_function(),
+                )
             metadata = []
             if update_fields:
                 metadata.append(("update_fields", ",".join(update_fields)))
@@ -444,9 +460,7 @@
             if is_sync_save:
                 metadata.append(("is_sync_save", "1"))
             self.stub.invoke(
-                "Update%s" % classname,
-                self._wrapped_class,
-                metadata=metadata,
+                "Update%s" % classname, self._wrapped_class, metadata=metadata
             )
         self.do_post_save_fixups()
 
@@ -648,7 +662,7 @@
         return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
 
     def get(self, **kwargs):
-        if kwargs.keys() == ["id"]:
+        if list(kwargs.keys()) == ["id"]:
             # the fast and easy case, look it up by id
             return self.wrap_single(
                 self._stub.invoke(
diff --git a/lib/xos-api/xosapi/version.py b/lib/xos-api/xosapi/version.py
index 2c84950..524d081 100644
--- a/lib/xos-api/xosapi/version.py
+++ b/lib/xos-api/xosapi/version.py
@@ -12,5 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This file will be replaced by setup.py
-__version__ = "unknown"
+from __future__ import absolute_import
+
+import os
+
+# read the version in from VERSION file, which is installed next to this file.
+with open(os.path.join(os.path.dirname(__file__), "VERSION")) as vf:
+    __version__ = vf.read()
diff --git a/lib/xos-api/xosapi/xos_grpc_client.py b/lib/xos-api/xosapi/xos_grpc_client.py
index a80843f..fba9ce4 100644
--- a/lib/xos-api/xosapi/xos_grpc_client.py
+++ b/lib/xos-api/xosapi/xos_grpc_client.py
@@ -12,35 +12,33 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
+
 import argparse
 import base64
 import functools
-import grpc
-import orm
+import inspect
 import os
 import sys
 
-from twisted.internet import reactor
 from google.protobuf.empty_pb2 import Empty
-from grpc import (
-    metadata_call_credentials,
-    composite_channel_credentials,
-    ssl_channel_credentials,
-)
 
-# fix up sys.path for chameleon
-import inspect
+import grpc
+from grpc import (composite_channel_credentials, metadata_call_credentials,
+                  ssl_channel_credentials)
 
-currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
-sys.path = [currentdir] + sys.path
-
+from twisted.internet import reactor
 from xosconfig import Config
-import chameleon_client.grpc_client as chameleon_client
+
+from xosapi import orm
+import xosapi.chameleon_client.grpc_client as chameleon_client
 
 from multistructlog import create_logger
 log = create_logger(Config().get("logging"))
 
+currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
+sys.path = [currentdir] + sys.path
+
 SERVER_CA = "/usr/local/share/ca-certificates/local_certs.crt"
 
 
@@ -97,7 +95,7 @@
                 for cm in response.convenience_methods:
                     log.debug("Saving convenience method", method=cm.filename)
                     save_path = os.path.join(convenience_methods_dir, cm.filename)
-                    file(save_path, "w").write(cm.contents)
+                    open(save_path, "w").write(cm.contents)
             else:
                 log.exception(
                     "Cannot load convenience methods, restarting the synchronzier"