PHOENIX-5880 Add SQLAchemy support to python driver

also:
* PHOENIX-5846 Let the python client parse options from the JDBC URL
* compatibility fixes for python < 3.6
* limited support for old setuptools
* use default SPNEGO config from requests_gssapi

Closes #32
diff --git a/python/phoenixdb/Dockerfile b/python/phoenixdb/Dockerfile
index 7edaced..e0beaee 100644
--- a/python/phoenixdb/Dockerfile
+++ b/python/phoenixdb/Dockerfile
@@ -3,7 +3,7 @@
 RUN apt-get update && apt-get install -y krb5-user libkrb5-dev
 
 ENV PHOENIXDB_TEST_DB_URL=http://host.docker.internal:8765
-ENV PHOENIXDB_TEST_DB_TRUSTSTORE $PHOENIXDB_TEST_DB_TRUSTSTORE
-ENV PHOENIXDB_TEST_DB_AUTHENTICATION $PHOENIXDB_TEST_DB_AUTHENTICATION
-ENV PHOENIXDB_TEST_DB_AVATICA_USER $PHOENIXDB_TEST_DB_AVATICA_USER
-ENV PHOENIXDB_TEST_DB_AVATICA_PASSWORD $PHOENIXDB_TEST_DB_AVATICA_PASSWORD
\ No newline at end of file
+ENV PHOENIXDB_TEST_DB_TRUSTSTORE=
+ENV PHOENIXDB_TEST_DB_AUTHENTICATION=
+ENV PHOENIXDB_TEST_DB_AVATICA_USER=
+ENV PHOENIXDB_TEST_DB_AVATICA_PASSWORD=
\ No newline at end of file
diff --git a/python/phoenixdb/NEWS.rst b/python/phoenixdb/NEWS.rst
index 089e7ed..db0ab0f 100644
--- a/python/phoenixdb/NEWS.rst
+++ b/python/phoenixdb/NEWS.rst
@@ -4,6 +4,7 @@
 Unreleased
 ----------
 - Replaced bundled requests_kerberos with request_gssapi library
+- Use default SPNEGO Auth settings from request_gssapi
 - Refactored authentication code
 - Added support for specifying server certificate
 - Added support for BASIC and DIGEST authentication
@@ -14,6 +15,7 @@
 - Refactored test suite
 - Removed shell example, as it was python2 only
 - Updated documentation
+- Added SQLAlchemy dialect
 
 Version 0.7
 -----------
diff --git a/python/phoenixdb/README.rst b/python/phoenixdb/README.rst
index 8f321af..793f8ee 100644
--- a/python/phoenixdb/README.rst
+++ b/python/phoenixdb/README.rst
@@ -57,12 +57,22 @@
     python setup.py develop
 
 You can start a Phoenix QueryServer instance on http://localhost:8765 for testing by running
-the following command in the phoenix-queryserver directory:
+the following command in the phoenix-queryserver directory::
 
     mvn clean verify -am -pl queryserver-it -Dtest=foo \
     -Dit.test=QueryServerBasicsIT\#startLocalPQS \
     -Ddo.not.randomize.pqs.port=true -Dstart.unsecure.pqs=true
 
+You can start a secure (https+kerberos) Phoenix QueryServer instance on https://localhost:8765
+for testing by running the following command in the phoenix-queryserver directory::
+
+    mvn clean verify -am -pl queryserver-it -Dtest=foo \
+    -Dit.test=SecureQueryServerPhoenixDBIT\#startLocalPQS \
+    -Ddo.not.randomize.pqs.port=true -Dstart.secure.pqs=true
+
+this will also create a shell script in queryserver-it/target/krb_setup.sh, that you can use to set
+up the environment for the tests.
+
 If you want to use the library without installing the phoenixdb library, you can use
 the `PYTHONPATH` environment variable to point to the library directly::
 
diff --git a/python/phoenixdb/phoenixdb/__init__.py b/python/phoenixdb/phoenixdb/__init__.py
index a90be99..4f5378e 100644
--- a/python/phoenixdb/phoenixdb/__init__.py
+++ b/python/phoenixdb/phoenixdb/__init__.py
@@ -12,7 +12,7 @@
 # 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.
-from gssapi import mechs
+import sys
 
 from phoenixdb import errors, types
 from phoenixdb.avatica import AvaticaClient
@@ -22,8 +22,13 @@
 
 from requests.auth import HTTPBasicAuth, HTTPDigestAuth
 
-from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
+from requests_gssapi import HTTPSPNEGOAuth
 
+if sys.version_info.major == 3:
+    from urllib.parse import urlencode, urlparse, urlunparse, parse_qs
+else:
+    from urllib import urlencode
+    from urlparse import urlparse, urlunparse, parse_qs
 
 __all__ = ['connect', 'apilevel', 'threadsafety', 'paramstyle'] + types.__all__ + errors.__all__
 
@@ -96,14 +101,41 @@
         :class:`~phoenixdb.connection.Connection` object.
     """
 
-    spnego = mechs.Mechanism.from_sasl_name("SPNEGO")
+    url_parsed = urlparse(url)
+    url_params = parse_qs(url_parsed.query, keep_blank_values=True)
+
+    # Parse supported JDBC compatible options from URL. args have precendece
+    rebuild = False
+    if auth is None and authentication is None and 'authentication' in url_params:
+        authentication = url_params['authentication'][0]
+        del url_params['authentication']
+        rebuild = True
+
+    if avatica_user is None and 'avatica_user' in url_params:
+        avatica_user = url_params['avatica_user'][0]
+        del url_params['avatica_user']
+        rebuild = True
+
+    if avatica_password is None and 'avatica_password' in url_params:
+        avatica_password = url_params['avatica_password'][0]
+        del url_params['avatica_password']
+        rebuild = True
+
+    if verify is None and truststore is None and 'truststore' in url_params:
+        truststore = url_params['truststore'][0]
+        del url_params['truststore']
+        rebuild = True
+
+    if rebuild:
+        url_parsed._replace(query=urlencode(url_params, True))
+        url = urlunparse(url_parsed)
 
     if auth == "SPNEGO":
         # Special case for backwards compatibility
-        auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL, mech=spnego)
+        auth = HTTPSPNEGOAuth(opportunistic_auth=True)
     elif auth is None and authentication is not None:
         if authentication == "SPNEGO":
-            auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL, mech=spnego, opportunistic_auth=True)
+            auth = HTTPSPNEGOAuth(opportunistic_auth=True)
         elif authentication == "BASIC" and avatica_user is not None and avatica_password is not None:
             auth = HTTPBasicAuth(avatica_user, avatica_password)
         elif authentication == "DIGEST" and avatica_user is not None and avatica_password is not None:
diff --git a/python/phoenixdb/phoenixdb/connection.py b/python/phoenixdb/phoenixdb/connection.py
index 1cd562b..0930115 100644
--- a/python/phoenixdb/phoenixdb/connection.py
+++ b/python/phoenixdb/phoenixdb/connection.py
@@ -130,9 +130,7 @@
         """Sets one or more parameters in the current connection.
 
         :param autocommit:
-            Switch the connection to autocommit mode. With the current
-            version, you need to always enable this, because
-            :meth:`commit` is not implemented.
+            Switch the connection to autocommit mode.
 
         :param readonly:
             Switch the connection to read-only mode.
diff --git a/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py b/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
new file mode 100644
index 0000000..1d5da0a
--- /dev/null
+++ b/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
@@ -0,0 +1,257 @@
+# Copyright 2017 Dimitri Capitaine
+#
+# 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.
+
+from sqlalchemy import types
+from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
+from sqlalchemy.exc import CompileError
+from sqlalchemy.sql.compiler import DDLCompiler
+from sqlalchemy.types import BIGINT, BOOLEAN, CHAR, DATE, DECIMAL, FLOAT, INTEGER, NUMERIC,\
+    SMALLINT, TIME, TIMESTAMP, VARBINARY, VARCHAR
+
+import phoenixdb
+import re
+import sys
+
+if sys.version_info.major == 3:
+    from urllib.parse import urlunsplit, SplitResult, urlencode
+else:
+    from urllib import urlencode
+    from urlparse import urlunsplit, SplitResult
+
+
+class PhoenixDDLCompiler(DDLCompiler):
+
+    def visit_primary_key_constraint(self, constraint):
+        if constraint.name is None:
+            raise CompileError("can't create primary key without a name")
+        return DDLCompiler.visit_primary_key_constraint(self, constraint)
+
+
+AUTOCOMMIT_REGEXP = re.compile(
+    r"\s*(?:UPDATE|UPSERT|CREATE|DELETE|DROP|ALTER)", re.I | re.UNICODE
+)
+
+
+class PhoenixExecutionContext(DefaultExecutionContext):
+
+    def should_autocommit_text(self, statement):
+        return AUTOCOMMIT_REGEXP.match(statement)
+
+
+class PhoenixDialect(DefaultDialect):
+    '''Phoenix dialect
+
+    dialect:: phoenix
+    :name: Phoenix
+
+    note::
+
+    The Phoenix dialect for SQLAlchemy is incomplete. It implements the functions required by Hue
+    for basic operation, but little else.
+
+    Connecting
+    ----------
+
+    The connection URL has the format of phoenix://host:port
+
+    This format does not allow for specifying the http scheme, or the URL path the the server uses.
+    Setting tls=True sets the server URL scheme to https.
+    If the path arg is set , it used as the path of the server URL.
+
+    The phoenix-specific authentication options can be set via the standard connect_args argument.
+
+    Connecting to an unsecure server::
+
+        create_engine('phoenix://localhost:8765')
+
+    Connecting to a secure server via SPNEGO (after kinit)::
+
+        create_engine('phoenix://localhost:8765', tls=True, connect_args={'authentication': 'SPNEGO'})
+
+    Connecting to a secure server via Knox::
+
+        create_engine('phoenix://localhost:8765', tls=True, path='/gateway/avatica/'\
+        connect_args={'authentication':'BASIC', 'avatica_user':'user', 'avatica_password':'password'})
+    '''
+
+    name = "phoenix"
+
+    driver = "phoenixdb"
+
+    ddl_compiler = PhoenixDDLCompiler
+
+    execution_ctx_cls = PhoenixExecutionContext
+
+    def __init__(self, tls=False, path='/', **opts):
+        # There is no way to pass these via the SqlAlchemy url object
+        self.tls = tls
+        self.path = path
+        super(PhoenixDialect, self).__init__(self, **opts)
+
+    @classmethod
+    def dbapi(cls):
+        return phoenixdb
+
+    def create_connect_args(self, url):
+        phoenix_url = urlunsplit(SplitResult(
+            scheme='https' if self.tls else 'http',
+            netloc='{}:{}'.format(url.host, 8765 if url.port is None else url.port),
+            path=self.path,
+            query=urlencode(url.query),
+            fragment='',
+        ))
+        return [phoenix_url], {}
+
+    def has_table(self, connection, table_name, schema=None):
+        if schema is None:
+            query = "SELECT 1 FROM system.catalog WHERE table_name = ? LIMIT 1"
+            params = [table_name.upper()]
+        else:
+            query = "SELECT 1 FROM system.catalog WHERE table_name = ? AND TABLE_SCHEM = ? LIMIT 1"
+            params = [table_name.upper(), schema.upper()]
+        return connection.execute(query, params).first() is not None
+
+    def get_schema_names(self, connection, **kw):
+        query = "SELECT DISTINCT TABLE_SCHEM FROM SYSTEM.CATALOG"
+        return [row[0] for row in connection.execute(query)]
+
+    def get_table_names(self, connection, schema=None, **kw):
+        if schema is None:
+            query = "SELECT DISTINCT table_name FROM SYSTEM.CATALOG"
+            params = []
+        else:
+            query = "SELECT DISTINCT table_name FROM SYSTEM.CATALOG WHERE TABLE_SCHEM = ? "
+            params = [schema.upper()]
+        return [row[0] for row in connection.execute(query, params)]
+
+    def get_columns(self, connection, table_name, schema=None, **kw):
+        if schema is None:
+            query = "SELECT COLUMN_NAME,  DATA_TYPE, NULLABLE " \
+                    "FROM system.catalog " \
+                    "WHERE table_name = ? " \
+                    "ORDER BY ORDINAL_POSITION"
+            params = [table_name.upper()]
+        else:
+            query = "SELECT COLUMN_NAME, DATA_TYPE, NULLABLE " \
+                    "FROM system.catalog " \
+                    "WHERE TABLE_SCHEM = ? " \
+                    "AND table_name = ? " \
+                    "ORDER BY ORDINAL_POSITION"
+            params = [schema.upper(), table_name.upper()]
+
+        # get all of the fields for this table
+        c = connection.execute(query, params)
+        cols = []
+        # first always none
+        c.fetchone()
+        while True:
+            row = c.fetchone()
+            if row is None:
+                break
+            name = row[0]
+            col_type = COLUMN_DATA_TYPE[row[1]]
+            nullable = row[2] == 1 if True else False
+
+            col_d = {
+                'name': name,
+                'type': col_type,
+                'nullable': nullable,
+                'default': None
+            }
+
+            cols.append(col_d)
+        return cols
+
+    # TODO This should be possible to implement
+    def get_pk_constraint(self, conn, table_name, schema=None, **kw):
+        return []
+
+    def get_foreign_keys(self, conn, table_name, schema=None, **kw):
+        return []
+
+    # TODO This should be possible to implement
+    def get_indexes(self, conn, table_name, schema=None, **kw):
+        return []
+
+
+class TINYINT(types.Integer):
+    __visit_name__ = "SMALLINT"
+
+
+class UNSIGNED_TINYINT(types.Integer):
+    __visit_name__ = "SMALLINT"
+
+
+class UNSIGNED_INTEGER(types.Integer):
+    __visit_name__ = "INTEGER"
+
+
+class DOUBLE(types.FLOAT):
+    __visit_name__ = "FLOAT"
+
+
+class UNSIGNED_DOUBLE(types.FLOAT):
+    __visit_name__ = "FLOAT"
+
+
+class UNSIGNED_FLOAT(types.FLOAT):
+    __visit_name__ = "FLOAT"
+
+
+class UNSIGNED_LONG(types.BIGINT):
+    __visit_name__ = "BIGINT"
+
+
+class UNSIGNED_TIME(types.TIME):
+    __visit_name__ = "TIME"
+
+
+class UNSIGNED_DATE(types.DATE):
+    __visit_name__ = "DATE"
+
+
+class UNSIGNED_TIMESTAMP(types.TIMESTAMP):
+    __visit_name__ = "TIMESTAMP"
+
+
+class ROWID (types.String):
+    __visit_name__ = "VARCHAR"
+
+
+COLUMN_DATA_TYPE = {
+    -6: TINYINT,
+    -5: BIGINT,
+    -3: VARBINARY,
+    1: CHAR,
+    2: NUMERIC,
+    3: DECIMAL,
+    4: INTEGER,
+    5: SMALLINT,
+    6: FLOAT,
+    8: DOUBLE,
+    9: UNSIGNED_INTEGER,
+    10: UNSIGNED_LONG,
+    11: UNSIGNED_TINYINT,
+    12: VARCHAR,
+    13: ROWID,
+    14: UNSIGNED_FLOAT,
+    15: UNSIGNED_DOUBLE,
+    16: BOOLEAN,
+    18: UNSIGNED_TIME,
+    19: UNSIGNED_DATE,
+    20: UNSIGNED_TIMESTAMP,
+    91: DATE,
+    92: TIME,
+    93: TIMESTAMP
+}
diff --git a/python/phoenixdb/phoenixdb/tests/__init__.py b/python/phoenixdb/phoenixdb/tests/__init__.py
index b541929..bacc8da 100644
--- a/python/phoenixdb/phoenixdb/tests/__init__.py
+++ b/python/phoenixdb/phoenixdb/tests/__init__.py
@@ -18,8 +18,7 @@
 
 import phoenixdb
 
-TEST_DB_URL = os.environ.get('PHOENIXDB_TEST_DB_URL')
-# TEST_DB_URL = "http://localhost:8765"
+TEST_DB_URL = os.environ.get('PHOENIXDB_TEST_DB_URL', "http://localhost:8765")
 TEST_DB_TRUSTSTORE = os.environ.get('PHOENIXDB_TEST_DB_TRUSTSTORE')
 TEST_DB_AUTHENTICATION = os.environ.get('PHOENIXDB_TEST_DB_AUTHENTICATION')
 TEST_DB_AVATICA_USER = os.environ.get('PHOENIXDB_TEST_DB_AVATICA_USER')
diff --git a/python/phoenixdb/phoenixdb/tests/test_db.py b/python/phoenixdb/phoenixdb/tests/test_db.py
index ea81e4a..04f19d9 100644
--- a/python/phoenixdb/phoenixdb/tests/test_db.py
+++ b/python/phoenixdb/phoenixdb/tests/test_db.py
@@ -83,5 +83,27 @@
             self.assertEqual(cursor.fetchall(), [[1, 'text 1']])
 
     def test_transaction(self):
-        # Todo write some transaction tests
-        pass
+        self.reopen(autocommit=False)
+        with self.conn.cursor() as cursor:
+            self.createTable("test", "CREATE TABLE {table} (id INTEGER PRIMARY KEY, text VARCHAR)")
+
+            cursor.execute("UPSERT INTO test VALUES (?, ?)", [1, 'one'])
+            cursor.execute("SELECT * FROM test ORDER BY id")
+            self.assertEqual(cursor.fetchall(), [])
+
+            self.conn.commit()
+            cursor.execute("SELECT * FROM test ORDER BY id")
+            self.assertEqual(cursor.fetchall(), [[1, 'one']])
+            self.assertEqual(self.conn.autocommit, False)
+
+            cursor.execute("UPSERT INTO test VALUES (?, ?)", [2, 'two'])
+            self.conn.rollback()
+            cursor.execute("SELECT * FROM test ORDER BY id")
+            self.assertEqual(cursor.fetchall(), [[1, 'one']])
+            self.assertEqual(self.conn.autocommit, False)
+
+            cursor.execute("UPSERT INTO test VALUES (?, ?)", [2, 'two'])
+            # Since we expose the JDBC semantics, this is an implicit commit
+            self.conn.autocommit = True
+            cursor.execute("SELECT * FROM test ORDER BY id")
+            self.assertEqual(cursor.fetchall(), [[1, 'one'], [2, 'two']])
diff --git a/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py b/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py
new file mode 100644
index 0000000..2cf7a57
--- /dev/null
+++ b/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 sys
+
+import sqlalchemy as db
+from sqlalchemy import text
+
+from . import TEST_DB_URL, TEST_DB_AUTHENTICATION, TEST_DB_AVATICA_USER, TEST_DB_AVATICA_PASSWORD,\
+        TEST_DB_TRUSTSTORE
+
+if sys.version_info.major == 3:
+    from urllib.parse import urlparse, urlunparse
+else:
+    from urlparse import urlparse, urlunparse
+
+
+@unittest.skipIf(TEST_DB_URL is None, "these tests require the PHOENIXDB_TEST_DB_URL environment variable set to a clean database")
+class SQLAlchemyTest(unittest.TestCase):
+
+    def test_connection(self):
+        engine = self._create_engine()
+        # connection = engine.connect()
+        metadata = db.MetaData()
+        catalog = db.Table('CATALOG', metadata, autoload=True, autoload_with=engine)
+        self.assertIn('TABLE_NAME', catalog.columns.keys())
+
+    def test_textual(self):
+        engine = self._create_engine()
+        with engine.connect() as connection:
+            try:
+                connection.execute('drop table if exists ALCHEMY_TEST')
+                connection.execute(text('create table ALCHEMY_TEST (id integer primary key)'))
+                connection.execute(text('upsert into ALCHEMY_TEST values (42)'))
+                # SQLAlchemy autocommit should kick in
+                result = connection.execute(text('select * from ALCHEMY_TEST'))
+                row = result.fetchone()
+                self.assertEqual(row[0], 42)
+            finally:
+                connection.execute('drop table if exists ALCHEMY_TEST')
+
+    @unittest.skip("ORM feature not implemented")
+    def test_orm(self):
+        pass
+
+    def _create_engine(self):
+        ''''Massage the properties that we use for the DBAPI tests so that they apply to
+        SQLAlchemy'''
+
+        url_parts = urlparse(TEST_DB_URL)
+
+        tls = url_parts.scheme.lower == 'https'
+
+        url_parts = url_parts._replace(scheme='phoenix')
+
+        connect_args = dict()
+        if TEST_DB_AUTHENTICATION:
+            connect_args.update(authentication=TEST_DB_AUTHENTICATION)
+        if TEST_DB_AVATICA_USER:
+            connect_args.update(avatica_user=TEST_DB_AVATICA_USER)
+        if TEST_DB_AVATICA_PASSWORD:
+            connect_args.update(avatica_password=TEST_DB_AVATICA_PASSWORD)
+        if TEST_DB_TRUSTSTORE:
+            connect_args.update(trustore=TEST_DB_TRUSTSTORE)
+
+        return db.create_engine(urlunparse(url_parts), tls=tls, connect_args=connect_args)
diff --git a/python/phoenixdb/setup.py b/python/phoenixdb/setup.py
index d1f0ce5..f7e3124 100644
--- a/python/phoenixdb/setup.py
+++ b/python/phoenixdb/setup.py
@@ -17,6 +17,8 @@
 # under the License.
 #
 from setuptools import setup, find_packages
+import setuptools
+import sys
 
 cmdclass = {}
 
@@ -32,6 +34,31 @@
         return f.read()
 
 
+if setuptools.__version__ < '20.8.1':
+    # Workaround for source install on old setuptools
+    # This won't be able to create a proper multi-version pacakage
+    install_requires=[
+        'protobuf>=3.0.0',
+        'requests',
+        'requests-gssapi',
+        'SQLAlchemy'
+    ]
+    if sys.version_info < (3,6):
+        install_requires.append('gssapi<1.6.0')
+    #Don't build the docs on an old stack
+    setup_requires=[]
+else:
+    install_requires=[
+        'protobuf>=3.0.0',
+        'requests',
+        'requests-gssapi',
+        'gssapi<1.6.0;python_version<"3.6"',
+        'SQLAlchemy'
+    ]
+    setup_requires=[
+        'Sphinx;python_version>="3.6"',
+    ],
+
 version = "1.0.0.dev"
 
 setup(
@@ -63,21 +90,19 @@
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
     ],
-    install_requires=[
-        'protobuf>=3.0.0',
-        'requests',
-        'requests-gssapi'
-    ],
+    install_requires=install_requires,
+    extras_require={
+        'SQLAlchemy': ['SQLAlchemy'],
+    },
     tests_require=[
+        'SQLAlchemy',
         'nose',
         'flake8'
     ],
-    setup_requires=[
-        # Later versions don't work with python2.7
-        'Sphinx<2.0.0',
-        # These are Sphinx dependencies, included only to be version managed for python2
-        'MarkupSafe<2.0.0',
-        'Jinja2<3.0.0',
-        'pyparsing<3.0.0'
-    ]
+    setup_requires=setup_requires,
+    entry_points={
+        "sqlalchemy.dialects": [
+            "phoenix = phoenixdb.sqlalchemy_phoenix:PhoenixDialect"
+        ]
+    },
 )
diff --git a/queryserver-it/src/it/java/org/apache/phoenix/end2end/SecureQueryServerPhoenixDBIT.java b/queryserver-it/src/it/java/org/apache/phoenix/end2end/SecureQueryServerPhoenixDBIT.java
index 95c32f5..b054105 100644
--- a/queryserver-it/src/it/java/org/apache/phoenix/end2end/SecureQueryServerPhoenixDBIT.java
+++ b/queryserver-it/src/it/java/org/apache/phoenix/end2end/SecureQueryServerPhoenixDBIT.java
@@ -245,7 +245,9 @@
         conf.set(QueryServerProperties.QUERY_SERVER_KEYTAB_FILENAME_ATTRIB,
             KEYTAB.getAbsolutePath());
         conf.setBoolean(QueryServerProperties.QUERY_SERVER_DISABLE_KERBEROS_LOGIN, true);
-        conf.setInt(QueryServerProperties.QUERY_SERVER_HTTP_PORT_ATTRIB, 0);
+        if(System.getProperty("do.not.randomize.pqs.port") == null) {
+            conf.setInt(QueryServerProperties.QUERY_SERVER_HTTP_PORT_ATTRIB, 0);
+        }
         // Required so that PQS can impersonate the end-users to HBase
         conf.set("hadoop.proxyuser.phoenixqs.groups", "*");
         conf.set("hadoop.proxyuser.phoenixqs.hosts", "*");