http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/connection.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/connection.py b/python/phoenixdb/connection.py
deleted file mode 100644
index 593a242..0000000
--- a/python/phoenixdb/connection.py
+++ /dev/null
@@ -1,187 +0,0 @@
-# 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 logging
-import uuid
-import weakref
-from phoenixdb import errors
-from phoenixdb.avatica.client import OPEN_CONNECTION_PROPERTIES
-from phoenixdb.cursor import Cursor
-from phoenixdb.errors import ProgrammingError
-
-__all__ = ['Connection']
-
-logger = logging.getLogger(__name__)
-
-
-class Connection(object):
-    """Database connection.
-
-    You should not construct this object manually, use 
:func:`~phoenixdb.connect` instead.
-    """
-
-    cursor_factory = None
-    """
-    The default cursor factory used by :meth:`cursor` if the parameter is not 
specified.
-    """
-
-    def __init__(self, client, cursor_factory=None, **kwargs):
-        self._client = client
-        self._closed = False
-        if cursor_factory is not None:
-            self.cursor_factory = cursor_factory
-        else:
-            self.cursor_factory = Cursor
-        self._cursors = []
-        # Extract properties to pass to OpenConnectionRequest
-        self._connection_args = {}
-        # The rest of the kwargs
-        self._filtered_args = {}
-        for k in kwargs:
-            if k in OPEN_CONNECTION_PROPERTIES:
-                self._connection_args[k] = kwargs[k]
-            else:
-                self._filtered_args[k] = kwargs[k]
-        self.open()
-        self.set_session(**self._filtered_args)
-
-    def __del__(self):
-        if not self._closed:
-            self.close()
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        if not self._closed:
-            self.close()
-
-    def open(self):
-        """Opens the connection."""
-        self._id = str(uuid.uuid4())
-        self._client.open_connection(self._id, info=self._connection_args)
-
-    def close(self):
-        """Closes the connection.
-        No further operations are allowed, either on the connection or any
-        of its cursors, once the connection is closed.
-
-        If the connection is used in a ``with`` statement, this method will
-        be automatically called at the end of the ``with`` block.
-        """
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        for cursor_ref in self._cursors:
-            cursor = cursor_ref()
-            if cursor is not None and not cursor._closed:
-                cursor.close()
-        self._client.close_connection(self._id)
-        self._client.close()
-        self._closed = True
-
-    @property
-    def closed(self):
-        """Read-only attribute specifying if the connection is closed or 
not."""
-        return self._closed
-
-    def commit(self):
-        """Commits pending database changes.
-
-        Currently, this does nothing, because the RPC does not support
-        transactions. Only defined for DB API 2.0 compatibility.
-        You need to use :attr:`autocommit` mode.
-        """
-        # TODO can support be added for this?
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-
-    def cursor(self, cursor_factory=None):
-        """Creates a new cursor.
-
-        :param cursor_factory:
-            This argument can be used to create non-standard cursors.
-            The class returned must be a subclass of
-            :class:`~phoenixdb.cursor.Cursor` (for example 
:class:`~phoenixdb.cursor.DictCursor`).
-            A default factory for the connection can also be specified using 
the
-            :attr:`cursor_factory` attribute.
-
-        :returns:
-            A :class:`~phoenixdb.cursor.Cursor` object.
-        """
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        cursor = (cursor_factory or self.cursor_factory)(self)
-        self._cursors.append(weakref.ref(cursor, self._cursors.remove))
-        return cursor
-
-    def set_session(self, autocommit=None, readonly=None):
-        """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.
-
-        :param readonly:
-            Switch the connection to read-only mode.
-        """
-        props = {}
-        if autocommit is not None:
-            props['autoCommit'] = bool(autocommit)
-        if readonly is not None:
-            props['readOnly'] = bool(readonly)
-        props = self._client.connection_sync(self._id, props)
-        self._autocommit = props.auto_commit
-        self._readonly = props.read_only
-        self._transactionisolation = props.transaction_isolation
-
-    @property
-    def autocommit(self):
-        """Read/write attribute for switching the connection's autocommit 
mode."""
-        return self._autocommit
-
-    @autocommit.setter
-    def autocommit(self, value):
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, {'autoCommit': 
bool(value)})
-        self._autocommit = props.auto_commit
-
-    @property
-    def readonly(self):
-        """Read/write attribute for switching the connection's readonly 
mode."""
-        return self._readonly
-
-    @readonly.setter
-    def readonly(self, value):
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, {'readOnly': 
bool(value)})
-        self._readonly = props.read_only
-
-    @property
-    def transactionisolation(self):
-        return self._transactionisolation
-
-    @transactionisolation.setter
-    def transactionisolation(self, value):
-        if self._closed:
-            raise ProgrammingError('the connection is already closed')
-        props = self._client.connection_sync(self._id, 
{'transactionIsolation': bool(value)})
-        self._transactionisolation = props.transaction_isolation
-
-
-for name in errors.__all__:
-    setattr(Connection, name, getattr(errors, name))

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/cursor.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/cursor.py b/python/phoenixdb/cursor.py
deleted file mode 100644
index 8be7bed..0000000
--- a/python/phoenixdb/cursor.py
+++ /dev/null
@@ -1,347 +0,0 @@
-# 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 logging
-import collections
-from phoenixdb.types import TypeHelper
-from phoenixdb.errors import ProgrammingError, InternalError
-from phoenixdb.avatica.proto import common_pb2
-
-__all__ = ['Cursor', 'ColumnDescription', 'DictCursor']
-
-logger = logging.getLogger(__name__)
-
-# TODO see note in Cursor.rowcount()
-MAX_INT = 2 ** 64 - 1
-
-ColumnDescription = collections.namedtuple('ColumnDescription', 'name 
type_code display_size internal_size precision scale null_ok')
-"""Named tuple for representing results from :attr:`Cursor.description`."""
-
-
-class Cursor(object):
-    """Database cursor for executing queries and iterating over results.
-
-    You should not construct this object manually, use 
:meth:`Connection.cursor() <phoenixdb.connection.Connection.cursor>` instead.
-    """
-
-    arraysize = 1
-    """
-    Read/write attribute specifying the number of rows to fetch
-    at a time with :meth:`fetchmany`. It defaults to 1 meaning to
-    fetch a single row at a time.
-    """
-
-    itersize = 2000
-    """
-    Read/write attribute specifying the number of rows to fetch
-    from the backend at each network roundtrip during iteration
-    on the cursor. The default is 2000.
-    """
-
-    def __init__(self, connection, id=None):
-        self._connection = connection
-        self._id = id
-        self._signature = None
-        self._column_data_types = []
-        self._frame = None
-        self._pos = None
-        self._closed = False
-        self.arraysize = self.__class__.arraysize
-        self.itersize = self.__class__.itersize
-        self._updatecount = -1
-
-    def __del__(self):
-        if not self._connection._closed and not self._closed:
-            self.close()
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        if not self._closed:
-            self.close()
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        row = self.fetchone()
-        if row is None:
-            raise StopIteration
-        return row
-
-    next = __next__
-
-    def close(self):
-        """Closes the cursor.
-        No further operations are allowed once the cursor is closed.
-
-        If the cursor is used in a ``with`` statement, this method will
-        be automatically called at the end of the ``with`` block.
-        """
-        if self._closed:
-            raise ProgrammingError('the cursor is already closed')
-        if self._id is not None:
-            self._connection._client.close_statement(self._connection._id, 
self._id)
-            self._id = None
-        self._signature = None
-        self._column_data_types = []
-        self._frame = None
-        self._pos = None
-        self._closed = True
-
-    @property
-    def closed(self):
-        """Read-only attribute specifying if the cursor is closed or not."""
-        return self._closed
-
-    @property
-    def description(self):
-        if self._signature is None:
-            return None
-        description = []
-        for column in self._signature.columns:
-            description.append(ColumnDescription(
-                column.column_name,
-                column.type.name,
-                column.display_size,
-                None,
-                column.precision,
-                column.scale,
-                None if column.nullable == 2 else bool(column.nullable),
-            ))
-        return description
-
-    def _set_id(self, id):
-        if self._id is not None and self._id != id:
-            self._connection._client.close_statement(self._connection._id, 
self._id)
-        self._id = id
-
-    def _set_signature(self, signature):
-        self._signature = signature
-        self._column_data_types = []
-        self._parameter_data_types = []
-        if signature is None:
-            return
-
-        for column in signature.columns:
-            dtype = TypeHelper.from_class(column.column_class_name)
-            self._column_data_types.append(dtype)
-
-        for parameter in signature.parameters:
-            dtype = TypeHelper.from_class(parameter.class_name)
-            self._parameter_data_types.append(dtype)
-
-    def _set_frame(self, frame):
-        self._frame = frame
-        self._pos = None
-
-        if frame is not None:
-            if frame.rows:
-                self._pos = 0
-            elif not frame.done:
-                raise InternalError('got an empty frame, but the statement is 
not done yet')
-
-    def _fetch_next_frame(self):
-        offset = self._frame.offset + len(self._frame.rows)
-        frame = self._connection._client.fetch(
-            self._connection._id, self._id,
-            offset=offset, frame_max_size=self.itersize)
-        self._set_frame(frame)
-
-    def _process_results(self, results):
-        if results:
-            result = results[0]
-            if result.own_statement:
-                self._set_id(result.statement_id)
-            self._set_signature(result.signature if 
result.HasField('signature') else None)
-            self._set_frame(result.first_frame if 
result.HasField('first_frame') else None)
-            self._updatecount = result.update_count
-
-    def _transform_parameters(self, parameters):
-        typed_parameters = []
-        for value, data_type in zip(parameters, self._parameter_data_types):
-            field_name, rep, mutate_to, cast_from = data_type
-            typed_value = common_pb2.TypedValue()
-
-            if value is None:
-                typed_value.null = True
-                typed_value.type = common_pb2.NULL
-            else:
-                typed_value.null = False
-
-                # use the mutator function
-                if mutate_to is not None:
-                    value = mutate_to(value)
-
-                typed_value.type = rep
-                setattr(typed_value, field_name, value)
-
-            typed_parameters.append(typed_value)
-        return typed_parameters
-
-    def execute(self, operation, parameters=None):
-        if self._closed:
-            raise ProgrammingError('the cursor is already closed')
-        self._updatecount = -1
-        self._set_frame(None)
-        if parameters is None:
-            if self._id is None:
-                
self._set_id(self._connection._client.create_statement(self._connection._id))
-            results = self._connection._client.prepare_and_execute(
-                self._connection._id, self._id,
-                operation, first_frame_max_size=self.itersize)
-            self._process_results(results)
-        else:
-            statement = self._connection._client.prepare(
-                self._connection._id, operation)
-            self._set_id(statement.id)
-            self._set_signature(statement.signature)
-
-            results = self._connection._client.execute(
-                self._connection._id, self._id,
-                statement.signature, self._transform_parameters(parameters),
-                first_frame_max_size=self.itersize)
-            self._process_results(results)
-
-    def executemany(self, operation, seq_of_parameters):
-        if self._closed:
-            raise ProgrammingError('the cursor is already closed')
-        self._updatecount = -1
-        self._set_frame(None)
-        statement = self._connection._client.prepare(
-            self._connection._id, operation, max_rows_total=0)
-        self._set_id(statement.id)
-        self._set_signature(statement.signature)
-        for parameters in seq_of_parameters:
-            self._connection._client.execute(
-                self._connection._id, self._id,
-                statement.signature, self._transform_parameters(parameters),
-                first_frame_max_size=0)
-
-    def _transform_row(self, row):
-        """Transforms a Row into Python values.
-
-        :param row:
-            A ``common_pb2.Row`` object.
-
-        :returns:
-            A list of values casted into the correct Python types.
-
-        :raises:
-            NotImplementedError
-        """
-        tmp_row = []
-
-        for i, column in enumerate(row.value):
-            if column.has_array_value:
-                raise NotImplementedError('array types are not supported')
-            elif column.scalar_value.null:
-                tmp_row.append(None)
-            else:
-                field_name, rep, mutate_to, cast_from = 
self._column_data_types[i]
-
-                # get the value from the field_name
-                value = getattr(column.scalar_value, field_name)
-
-                # cast the value
-                if cast_from is not None:
-                    value = cast_from(value)
-
-                tmp_row.append(value)
-        return tmp_row
-
-    def fetchone(self):
-        if self._frame is None:
-            raise ProgrammingError('no select statement was executed')
-        if self._pos is None:
-            return None
-        rows = self._frame.rows
-        row = self._transform_row(rows[self._pos])
-        self._pos += 1
-        if self._pos >= len(rows):
-            self._pos = None
-            if not self._frame.done:
-                self._fetch_next_frame()
-        return row
-
-    def fetchmany(self, size=None):
-        if size is None:
-            size = self.arraysize
-        rows = []
-        while size > 0:
-            row = self.fetchone()
-            if row is None:
-                break
-            rows.append(row)
-            size -= 1
-        return rows
-
-    def fetchall(self):
-        rows = []
-        while True:
-            row = self.fetchone()
-            if row is None:
-                break
-            rows.append(row)
-        return rows
-
-    def setinputsizes(self, sizes):
-        pass
-
-    def setoutputsize(self, size, column=None):
-        pass
-
-    @property
-    def connection(self):
-        """Read-only attribute providing access to the :class:`Connection 
<phoenixdb.connection.Connection>`
-        object this cursor was created from."""
-        return self._connection
-
-    @property
-    def rowcount(self):
-        """Read-only attribute specifying the number of rows affected by
-        the last executed DML statement or -1 if the number cannot be
-        determined. Note that this will always be set to -1 for select
-        queries."""
-        # TODO instead of -1, this ends up being set to Integer.MAX_VALUE
-        if self._updatecount == MAX_INT:
-            return -1
-        return self._updatecount
-
-    @property
-    def rownumber(self):
-        """Read-only attribute providing the current 0-based index of the
-        cursor in the result set or ``None`` if the index cannot be
-        determined.
-
-        The index can be seen as index of the cursor in a sequence
-        (the result set). The next fetch operation will fetch the
-        row indexed by :attr:`rownumber` in that sequence.
-        """
-        if self._frame is not None and self._pos is not None:
-            return self._frame.offset + self._pos
-        return self._pos
-
-
-class DictCursor(Cursor):
-    """A cursor which returns results as a dictionary"""
-
-    def _transform_row(self, row):
-        row = super(DictCursor, self)._transform_row(row)
-        d = {}
-        for ind, val in enumerate(row):
-            d[self._signature.columns[ind].column_name] = val
-        return d

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/doc/Makefile
----------------------------------------------------------------------
diff --git a/python/phoenixdb/doc/Makefile b/python/phoenixdb/doc/Makefile
new file mode 100644
index 0000000..31eb086
--- /dev/null
+++ b/python/phoenixdb/doc/Makefile
@@ -0,0 +1,192 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx 
installed, then set the SPHINXBUILD environment variable to point to the full 
path of the '$(SPHINXBUILD)' executable. Alternatively you can add the 
directory with the executable to your PATH. If you don't have Sphinx installed, 
grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp 
epub latex latexpdf text man changes linkcheck doctest coverage gettext
+
+help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html       to make standalone HTML files"
+       @echo "  dirhtml    to make HTML files named index.html in directories"
+       @echo "  singlehtml to make a single large HTML file"
+       @echo "  pickle     to make pickle files"
+       @echo "  json       to make JSON files"
+       @echo "  htmlhelp   to make HTML files and a HTML help project"
+       @echo "  qthelp     to make HTML files and a qthelp project"
+       @echo "  applehelp  to make an Apple Help Book"
+       @echo "  devhelp    to make HTML files and a Devhelp project"
+       @echo "  epub       to make an epub"
+       @echo "  latex      to make LaTeX files, you can set PAPER=a4 or 
PAPER=letter"
+       @echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+       @echo "  latexpdfja to make LaTeX files and run them through 
platex/dvipdfmx"
+       @echo "  text       to make text files"
+       @echo "  man        to make manual pages"
+       @echo "  texinfo    to make Texinfo files"
+       @echo "  info       to make Texinfo files and run them through makeinfo"
+       @echo "  gettext    to make PO message catalogs"
+       @echo "  changes    to make an overview of all changed/added/deprecated 
items"
+       @echo "  xml        to make Docutils-native XML files"
+       @echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+       @echo "  linkcheck  to check all external links for integrity"
+       @echo "  doctest    to run all doctests embedded in the documentation 
(if enabled)"
+       @echo "  coverage   to run coverage check of the documentation (if 
enabled)"
+
+clean:
+       rm -rf $(BUILDDIR)/*
+
+html:
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+       $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+       @echo
+       @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+
+json:
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" 
\
+             ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+       @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/phoenixdb.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/phoenixdb.qhc"
+
+applehelp:
+       $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+       @echo
+       @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+       @echo "N.B. You won't be able to view it unless you put it in" \
+             "~/Library/Documentation/Help or install it in your application" \
+             "bundle."
+
+devhelp:
+       $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+       @echo
+       @echo "Build finished."
+       @echo "To view the help file:"
+       @echo "# mkdir -p $$HOME/.local/share/devhelp/phoenixdb"
+       @echo "# ln -s $(BUILDDIR)/devhelp 
$$HOME/.local/share/devhelp/phoenixdb"
+       @echo "# devhelp"
+
+epub:
+       $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+       @echo
+       @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+       @echo "Run \`make' in that directory to run these through (pdf)latex" \
+             "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo "Running LaTeX files through pdflatex..."
+       $(MAKE) -C $(BUILDDIR)/latex all-pdf
+       @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo "Running LaTeX files through platex and dvipdfmx..."
+       $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+       @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+       $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+       @echo
+       @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+       $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+       @echo
+       @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+       $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+       @echo
+       @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+       @echo "Run \`make' in that directory to run these through makeinfo" \
+             "(use \`make info' here to do that automatically)."
+
+info:
+       $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+       @echo "Running Texinfo files through makeinfo..."
+       make -C $(BUILDDIR)/texinfo info
+       @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+       $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+       @echo
+       @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+       @echo
+       @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in $(BUILDDIR)/doctest/output.txt."
+
+coverage:
+       $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+       @echo "Testing of coverage in the sources finished, look at the " \
+             "results in $(BUILDDIR)/coverage/python.txt."
+
+xml:
+       $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+       @echo
+       @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+       $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+       @echo
+       @echo "Build finished. The pseudo-XML files are in 
$(BUILDDIR)/pseudoxml."

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/doc/api.rst
----------------------------------------------------------------------
diff --git a/python/phoenixdb/doc/api.rst b/python/phoenixdb/doc/api.rst
new file mode 100644
index 0000000..cac317c
--- /dev/null
+++ b/python/phoenixdb/doc/api.rst
@@ -0,0 +1,30 @@
+API Reference
+=============
+
+phoenixdb module
+----------------
+
+.. automodule:: phoenixdb
+    :members:
+    :undoc-members:
+
+phoenixdb.connection module
+---------------------------
+
+.. automodule:: phoenixdb.connection
+    :members:
+    :undoc-members:
+
+phoenixdb.cursor module
+-----------------------
+
+.. automodule:: phoenixdb.cursor
+    :members:
+    :undoc-members:
+
+phoenixdb.avatica module
+------------------------
+
+.. automodule:: phoenixdb.avatica
+    :members:
+    :undoc-members:

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/doc/conf.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/doc/conf.py b/python/phoenixdb/doc/conf.py
new file mode 100644
index 0000000..21898d7
--- /dev/null
+++ b/python/phoenixdb/doc/conf.py
@@ -0,0 +1,287 @@
+# -*- coding: utf-8 -*-
+#
+# phoenixdb documentation build configuration file, created by
+# sphinx-quickstart on Sun Jun 28 18:07:35 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+import shlex
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../phoenixdb'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.intersphinx',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'phoenixdb'
+copyright = u'2015, Lukas Lalinsky'
+author = u'Lukas Lalinsky'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'classic'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'phoenixdbdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+#latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+#}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+#latex_documents = [
+#  (master_doc, 'phoenixdb.tex', u'phoenixdb Documentation',
+#   u'Lukas Lalinsky', 'manual'),
+#]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'phoenixdb', u'phoenixdb Documentation',
+     [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  (master_doc, 'phoenixdb', u'phoenixdb Documentation',
+   author, 'phoenixdb', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/doc/index.rst
----------------------------------------------------------------------
diff --git a/python/phoenixdb/doc/index.rst b/python/phoenixdb/doc/index.rst
new file mode 100644
index 0000000..ada7fb8
--- /dev/null
+++ b/python/phoenixdb/doc/index.rst
@@ -0,0 +1,27 @@
+.. include:: ../README.rst
+
+API Reference
+-------------
+
+.. toctree::
+   :maxdepth: 2
+
+   api
+
+Changelog
+-------------
+
+.. toctree::
+   :maxdepth: 2
+
+   versions
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+
+.. _

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/doc/versions.rst
----------------------------------------------------------------------
diff --git a/python/phoenixdb/doc/versions.rst 
b/python/phoenixdb/doc/versions.rst
new file mode 100644
index 0000000..f3830fd
--- /dev/null
+++ b/python/phoenixdb/doc/versions.rst
@@ -0,0 +1,3 @@
+.. include:: ../NEWS.rst
+
+.. _

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/docker-compose.yml
----------------------------------------------------------------------
diff --git a/python/phoenixdb/docker-compose.yml 
b/python/phoenixdb/docker-compose.yml
new file mode 100644
index 0000000..bf398ec
--- /dev/null
+++ b/python/phoenixdb/docker-compose.yml
@@ -0,0 +1,21 @@
+# 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.
+
+version: "3"
+services:
+  phoenix:
+    image: 
docker.oxygene.sk/lukas/python-phoenixdb/phoenix:${PHOENIX_VERSION:-4.11}
+    ports:
+      - "127.0.0.1:8765:8765"

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/errors.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/errors.py b/python/phoenixdb/errors.py
deleted file mode 100644
index a046c0d..0000000
--- a/python/phoenixdb/errors.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# 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.
-
-__all__ = [
-    'Warning', 'Error', 'InterfaceError', 'DatabaseError', 'DataError',
-    'OperationalError', 'IntegrityError', 'InternalError',
-    'ProgrammingError', 'NotSupportedError',
-]
-
-try:
-    _StandardError = StandardError
-except NameError:
-    _StandardError = Exception
-
-
-class Warning(_StandardError):
-    """Not used by this package, only defined for compatibility
-    with DB API 2.0."""
-
-
-class Error(_StandardError):
-    """Exception that is the base class of all other error exceptions.
-    You can use this to catch all errors with one single except statement."""
-
-    def __init__(self, message, code=None, sqlstate=None, cause=None):
-        super(_StandardError, self).__init__(message, code, sqlstate, cause)
-
-    @property
-    def message(self):
-        return self.args[0]
-
-    @property
-    def code(self):
-        return self.args[1]
-
-    @property
-    def sqlstate(self):
-        return self.args[2]
-
-    @property
-    def cause(self):
-        return self.args[3]
-
-
-class InterfaceError(Error):
-    """Exception raised for errors that are related to the database
-    interface rather than the database itself."""
-
-
-class DatabaseError(Error):
-    """Exception raised for errors that are related to the database."""
-
-
-class DataError(DatabaseError):
-    """Exception raised for errors that are due to problems with the
-    processed data like division by zero, numeric value out of range,
-    etc."""
-
-
-class OperationalError(DatabaseError):
-    """Raised for errors that are related to the database's operation and not
-    necessarily under the control of the programmer, e.g. an unexpected
-    disconnect occurs, the data source name is not found, a transaction could
-    not be processed, a memory allocation error occurred during
-    processing, etc."""
-
-
-class IntegrityError(DatabaseError):
-    """Raised when the relational integrity of the database is affected, e.g. 
a foreign key check fails."""
-
-
-class InternalError(DatabaseError):
-    """Raised when the database encounters an internal problem."""
-
-
-class ProgrammingError(DatabaseError):
-    """Raises for programming errors, e.g. table not found, syntax error, 
etc."""
-
-
-class NotSupportedError(DatabaseError):
-    """Raised when using an API that is not supported by the database."""

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/examples/basic.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/examples/basic.py 
b/python/phoenixdb/examples/basic.py
new file mode 100755
index 0000000..4894d21
--- /dev/null
+++ b/python/phoenixdb/examples/basic.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+# 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 phoenixdb
+
+with phoenixdb.connect('http://localhost:8765/', autocommit=True) as 
connection:
+    with connection.cursor() as cursor:
+        cursor.execute("DROP TABLE IF EXISTS test")
+        cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, text 
VARCHAR)")
+        cursor.executemany("UPSERT INTO test VALUES (?, ?)", [[1, 'hello'], 
[2, 'world']])
+        cursor.execute("SELECT * FROM test ORDER BY id")
+        for row in cursor:
+            print(row)

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/examples/shell.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/examples/shell.py 
b/python/phoenixdb/examples/shell.py
new file mode 100755
index 0000000..820435e
--- /dev/null
+++ b/python/phoenixdb/examples/shell.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+
+# 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 logging
+import argparse
+import sqlline
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--debug', '-d', action='store_true')
+parser.add_argument('url')
+args = parser.parse_args()
+
+if args.debug:
+    logging.basicConfig(level=logging.DEBUG)
+
+with sqlline.SqlLine() as sqlline:
+    sqlline.connect('phoenixdb', args.url)
+    sqlline.connection.autocommit = True
+    sqlline.run()

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/gen-protobuf.sh
----------------------------------------------------------------------
diff --git a/python/phoenixdb/gen-protobuf.sh b/python/phoenixdb/gen-protobuf.sh
new file mode 100755
index 0000000..ee094ce
--- /dev/null
+++ b/python/phoenixdb/gen-protobuf.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+# 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.
+
+set -x
+AVATICA_VER=rel/avatica-1.10.0
+
+set -e
+
+rm -rf avatica-tmp
+
+mkdir avatica-tmp
+cd avatica-tmp
+wget -O avatica.tar.gz 
https://github.com/apache/calcite-avatica/archive/$AVATICA_VER.tar.gz
+tar -x --strip-components=1 -f avatica.tar.gz
+
+cd ..
+rm -f phoenixdb/avatica/proto/*_pb2.py
+protoc --proto_path=avatica-tmp/core/src/main/protobuf/ 
--python_out=phoenixdb/avatica/proto avatica-tmp/core/src/main/protobuf/*.proto
+if [[ "$(uname)" == "Darwin" ]]; then
+  sed -i '' 's/import common_pb2/from . import common_pb2/' 
phoenixdb/avatica/proto/*_pb2.py
+else
+  sed -i 's/import common_pb2/from . import common_pb2/' 
phoenixdb/avatica/proto/*_pb2.py
+fi
+
+rm -rf avatica-tmp

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/phoenixdb/__init__.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/phoenixdb/__init__.py 
b/python/phoenixdb/phoenixdb/__init__.py
new file mode 100644
index 0000000..24cb370
--- /dev/null
+++ b/python/phoenixdb/phoenixdb/__init__.py
@@ -0,0 +1,72 @@
+# 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.
+
+from phoenixdb import errors, types
+from phoenixdb.avatica import AvaticaClient
+from phoenixdb.connection import Connection
+from phoenixdb.errors import *  # noqa: F401,F403
+from phoenixdb.types import *  # noqa: F401,F403
+
+__all__ = ['connect', 'apilevel', 'threadsafety', 'paramstyle'] + 
types.__all__ + errors.__all__
+
+
+apilevel = "2.0"
+"""
+This module supports the `DB API 2.0 interface 
<https://www.python.org/dev/peps/pep-0249/>`_.
+"""
+
+threadsafety = 1
+"""
+Multiple threads can share the module, but neither connections nor cursors.
+"""
+
+paramstyle = 'qmark'
+"""
+Parmetrized queries should use the question mark as a parameter placeholder.
+
+For example::
+
+ cursor.execute("SELECT * FROM table WHERE id = ?", [my_id])
+"""
+
+
+def connect(url, max_retries=None, auth=None, **kwargs):
+    """Connects to a Phoenix query server.
+
+    :param url:
+        URL to the Phoenix query server, e.g. ``http://localhost:8765/``
+
+    :param autocommit:
+        Switch the connection to autocommit mode.
+
+    :param readonly:
+        Switch the connection to readonly mode.
+
+    :param max_retries:
+        The maximum number of retries in case there is a connection error.
+
+    :param cursor_factory:
+        If specified, the connection's 
:attr:`~phoenixdb.connection.Connection.cursor_factory` is set to it.
+
+    :param auth
+        If specified a specific auth type will be used, otherwise connection 
will be unauthenticated
+        Currently only SPNEGO is supported
+
+    :returns:
+        :class:`~phoenixdb.connection.Connection` object.
+    """
+    client = AvaticaClient(url, max_retries=max_retries, auth=auth)
+    client.connect()
+    return Connection(client, **kwargs)

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/phoenixdb/avatica/__init__.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/phoenixdb/avatica/__init__.py 
b/python/phoenixdb/phoenixdb/avatica/__init__.py
new file mode 100644
index 0000000..53776d7
--- /dev/null
+++ b/python/phoenixdb/phoenixdb/avatica/__init__.py
@@ -0,0 +1,16 @@
+# 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.
+
+from .client import AvaticaClient  # noqa: F401

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/phoenixdb/avatica/client.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/phoenixdb/avatica/client.py 
b/python/phoenixdb/phoenixdb/avatica/client.py
new file mode 100644
index 0000000..46f349f
--- /dev/null
+++ b/python/phoenixdb/phoenixdb/avatica/client.py
@@ -0,0 +1,502 @@
+# Copyright 2015 Lukas Lalinsky
+#
+# 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.
+
+"""Implementation of the JSON-over-HTTP RPC protocol used by Avatica."""
+
+import re
+import socket
+import pprint
+import math
+import logging
+import time
+from phoenixdb import errors
+from phoenixdb.avatica.proto import requests_pb2, common_pb2, responses_pb2
+
+import requests
+#from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
+from requests_kerberos import HTTPKerberosAuth, OPTIONAL
+import kerberos
+
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+
+try:
+    from HTMLParser import HTMLParser
+except ImportError:
+    from html.parser import HTMLParser
+
+__all__ = ['AvaticaClient']
+
+logger = logging.getLogger(__name__)
+
+
+class JettyErrorPageParser(HTMLParser):
+
+    def __init__(self):
+        HTMLParser.__init__(self)
+        self.path = []
+        self.title = []
+        self.message = []
+
+    def handle_starttag(self, tag, attrs):
+        self.path.append(tag)
+
+    def handle_endtag(self, tag):
+        self.path.pop()
+
+    def handle_data(self, data):
+        if len(self.path) > 2 and self.path[0] == 'html' and self.path[1] == 
'body':
+            if len(self.path) == 3 and self.path[2] == 'h2':
+                self.title.append(data.strip())
+            elif len(self.path) == 4 and self.path[2] == 'p' and self.path[3] 
== 'pre':
+                self.message.append(data.strip())
+
+
+def parse_url(url):
+    url = urlparse.urlparse(url)
+    if not url.scheme and not url.netloc and url.path:
+        netloc = url.path
+        if ':' not in netloc:
+            netloc = '{}:8765'.format(netloc)
+        return urlparse.ParseResult('http', netloc, '/', '', '', '')
+    return url
+
+
+# Defined in 
phoenix-core/src/main/java/org/apache/phoenix/exception/SQLExceptionCode.java
+SQLSTATE_ERROR_CLASSES = [
+    ('08', errors.OperationalError),  # Connection Exception
+    ('22018', errors.IntegrityError),  # Constraint violatioin.
+    ('22', errors.DataError),  # Data Exception
+    ('23', errors.IntegrityError),  # Constraint Violation
+    ('24', errors.InternalError),  # Invalid Cursor State
+    ('25', errors.InternalError),  # Invalid Transaction State
+    ('42', errors.ProgrammingError),  # Syntax Error or Access Rule Violation
+    ('XLC', errors.OperationalError),  # Execution exceptions
+    ('INT', errors.InternalError),  # Phoenix internal error
+]
+
+# Relevant properties as defined by 
https://calcite.apache.org/avatica/docs/client_reference.html
+OPEN_CONNECTION_PROPERTIES = (
+    'user',  # User for the database connection
+    'password',  # Password for the user
+)
+
+
+def raise_sql_error(code, sqlstate, message):
+    for prefix, error_class in SQLSTATE_ERROR_CLASSES:
+        if sqlstate.startswith(prefix):
+            raise error_class(message, code, sqlstate)
+
+
+def parse_and_raise_sql_error(message):
+    match = re.findall(r'(?:([^ ]+): )?ERROR (\d+) \(([0-9A-Z]{5})\): (.*?) 
->', message)
+    if match is not None and len(match):
+        exception, code, sqlstate, message = match[0]
+        raise_sql_error(int(code), sqlstate, message)
+
+
+def parse_error_page(html):
+    parser = JettyErrorPageParser()
+    parser.feed(html)
+    if parser.title == ['HTTP ERROR: 500']:
+        message = ' '.join(parser.message).strip()
+        parse_and_raise_sql_error(message)
+        raise errors.InternalError(message)
+
+
+def parse_error_protobuf(text):
+    message = common_pb2.WireMessage()
+    message.ParseFromString(text)
+
+    err = responses_pb2.ErrorResponse()
+    err.ParseFromString(message.wrapped_message)
+
+    parse_and_raise_sql_error(err.error_message)
+    raise_sql_error(err.error_code, err.sql_state, err.error_message)
+    raise errors.InternalError(err.error_message)
+
+
+class AvaticaClient(object):
+    """Client for Avatica's RPC server.
+
+    This exposes all low-level functionality that the Avatica
+    server provides, using the native terminology. You most likely
+    do not want to use this class directly, but rather get connect
+    to a server using :func:`phoenixdb.connect`.
+    """
+
+    def __init__(self, url, max_retries=None, auth=None):
+        """Constructs a new client object.
+
+        :param url:
+            URL of an Avatica RPC server.
+        """
+        self.url = parse_url(url)
+        self.max_retries = max_retries if max_retries is not None else 3
+        self.auth = auth
+        self.connection = None
+
+    def connect(self):
+        """This method used to open a persistent TCP connection
+        requests does not require this"""
+        pass
+
+    def close(self):
+        """Also does nothing per requests"""
+        pass
+
+    def _post_request(self, body, headers):
+        retry_count = self.max_retries
+        while True:
+            logger.debug("POST %s %r %r", self.url.geturl(), body, headers)
+            try:
+                if self.auth == "SPNEGO":
+                    #response = requests.request('post', self.url.geturl(), 
data=body, stream=True, headers=headers, 
auth=HTTPSPNEGOAuth(mutual_authentication=OPTIONAL))
+                    response = requests.request('post', self.url.geturl(), 
data=body, stream=True, headers=headers, 
auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL, 
mech_oid=kerberos.GSS_MECH_OID_SPNEGO))
+                else:
+                    response = requests.request('post', self.url.geturl(), 
data=body, stream=True, headers=headers)
+
+            except requests.HTTPError as e:
+                if retry_count > 0:
+                    delay = math.exp(-retry_count)
+                    logger.debug("HTTP protocol error, will retry in %s 
seconds...", delay, exc_info=True)
+                    time.sleep(delay)
+                    retry_count -= 1
+                    continue
+                raise errors.InterfaceError('RPC request failed', cause=e)
+            else:
+                if response.status_code == requests.codes.service_unavailable:
+                    if retry_count > 0:
+                        delay = math.exp(-retry_count)
+                        logger.debug("Service unavailable, will retry in %s 
seconds...", delay, exc_info=True)
+                        time.sleep(delay)
+                        retry_count -= 1
+                        continue
+                return response
+
+    def _apply(self, request_data, expected_response_type=None):
+        logger.debug("Sending request\n%s", pprint.pformat(request_data))
+
+        request_name = request_data.__class__.__name__
+        message = common_pb2.WireMessage()
+        message.name = 
'org.apache.calcite.avatica.proto.Requests${}'.format(request_name)
+        message.wrapped_message = request_data.SerializeToString()
+        body = message.SerializeToString()
+        headers = {'content-type': 'application/x-google-protobuf'}
+
+        response = self._post_request(body, headers)
+        response_body = response.raw.read()
+
+        if response.status_code != requests.codes.ok:
+            logger.debug("Received response\n%s", response_body)
+            if b'<html>' in response_body:
+                parse_error_page(response_body)
+            else:
+                # assume the response is in protobuf format
+                parse_error_protobuf(response_body)
+            raise errors.InterfaceError('RPC request returned invalid status 
code', response.status_code)
+
+        message = common_pb2.WireMessage()
+        message.ParseFromString(response_body)
+
+        logger.debug("Received response\n%s", message)
+
+        if expected_response_type is None:
+            expected_response_type = request_name.replace('Request', 
'Response')
+
+        expected_response_type = 'org.apache.calcite.avatica.proto.Responses$' 
+ expected_response_type
+        if message.name != expected_response_type:
+            raise errors.InterfaceError('unexpected response type "{}" 
expected "{}"'.format(message.name, expected_response_type))
+
+        return message.wrapped_message
+
+    def get_catalogs(self, connection_id):
+        request = requests_pb2.CatalogsRequest()
+        request.connection_id = connection_id
+        return self._apply(request)
+
+    def get_schemas(self, connection_id, catalog=None, schemaPattern=None):
+        request = requests_pb2.SchemasRequest()
+        request.connection_id = connection_id
+        if catalog is not None:
+            request.catalog = catalog
+        if schemaPattern is not None:
+            request.schema_pattern = schemaPattern
+        return self._apply(request)
+
+    def get_tables(self, connection_id, catalog=None, schemaPattern=None, 
tableNamePattern=None, typeList=None):
+        request = requests_pb2.TablesRequest()
+        request.connection_id = connection_id
+        if catalog is not None:
+            request.catalog = catalog
+        if schemaPattern is not None:
+            request.schema_pattern = schemaPattern
+        if tableNamePattern is not None:
+            request.table_name_pattern = tableNamePattern
+        if typeList is not None:
+            request.type_list = typeList
+        if typeList is not None:
+            request.type_list.extend(typeList)
+        request.has_type_list = typeList is not None
+        return self._apply(request)
+
+    def get_columns(self, connection_id, catalog=None, schemaPattern=None, 
tableNamePattern=None, columnNamePattern=None):
+        request = requests_pb2.ColumnsRequest()
+        request.connection_id = connection_id
+        if catalog is not None:
+            request.catalog = catalog
+        if schemaPattern is not None:
+            request.schema_pattern = schemaPattern
+        if tableNamePattern is not None:
+            request.table_name_pattern = tableNamePattern
+        if columnNamePattern is not None:
+            request.column_name_pattern = columnNamePattern
+        return self._apply(request)
+
+    def get_table_types(self, connection_id):
+        request = requests_pb2.TableTypesRequest()
+        request.connection_id = connection_id
+        return self._apply(request)
+
+    def get_type_info(self, connection_id):
+        request = requests_pb2.TypeInfoRequest()
+        request.connection_id = connection_id
+        return self._apply(request)
+
+    def connection_sync(self, connection_id, connProps=None):
+        """Synchronizes connection properties with the server.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param connProps:
+            Dictionary with the properties that should be changed.
+
+        :returns:
+            A ``common_pb2.ConnectionProperties`` object.
+        """
+        if connProps is None:
+            connProps = {}
+
+        request = requests_pb2.ConnectionSyncRequest()
+        request.connection_id = connection_id
+        request.conn_props.auto_commit = connProps.get('autoCommit', False)
+        request.conn_props.has_auto_commit = True
+        request.conn_props.read_only = connProps.get('readOnly', False)
+        request.conn_props.has_read_only = True
+        request.conn_props.transaction_isolation = 
connProps.get('transactionIsolation', 0)
+        request.conn_props.catalog = connProps.get('catalog', '')
+        request.conn_props.schema = connProps.get('schema', '')
+
+        response_data = self._apply(request)
+        response = responses_pb2.ConnectionSyncResponse()
+        response.ParseFromString(response_data)
+        return response.conn_props
+
+    def open_connection(self, connection_id, info=None):
+        """Opens a new connection.
+
+        :param connection_id:
+            ID of the connection to open.
+        """
+        request = requests_pb2.OpenConnectionRequest()
+        request.connection_id = connection_id
+        if info is not None:
+            # Info is a list of repeated pairs, setting a dict directly fails
+            for k, v in info.items():
+                request.info[k] = v
+
+        response_data = self._apply(request)
+        response = responses_pb2.OpenConnectionResponse()
+        response.ParseFromString(response_data)
+
+    def close_connection(self, connection_id):
+        """Closes a connection.
+
+        :param connection_id:
+            ID of the connection to close.
+        """
+        request = requests_pb2.CloseConnectionRequest()
+        request.connection_id = connection_id
+        self._apply(request)
+
+    def create_statement(self, connection_id):
+        """Creates a new statement.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :returns:
+            New statement ID.
+        """
+        request = requests_pb2.CreateStatementRequest()
+        request.connection_id = connection_id
+
+        response_data = self._apply(request)
+        response = responses_pb2.CreateStatementResponse()
+        response.ParseFromString(response_data)
+        return response.statement_id
+
+    def close_statement(self, connection_id, statement_id):
+        """Closes a statement.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param statement_id:
+            ID of the statement to close.
+        """
+        request = requests_pb2.CloseStatementRequest()
+        request.connection_id = connection_id
+        request.statement_id = statement_id
+
+        self._apply(request)
+
+    def prepare_and_execute(self, connection_id, statement_id, sql, 
max_rows_total=None, first_frame_max_size=None):
+        """Prepares and immediately executes a statement.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param statement_id:
+            ID of the statement to prepare.
+
+        :param sql:
+            SQL query.
+
+        :param max_rows_total:
+            The maximum number of rows that will be allowed for this query.
+
+        :param first_frame_max_size:
+            The maximum number of rows that will be returned in the first 
Frame returned for this query.
+
+        :returns:
+            Result set with the signature of the prepared statement and the 
first frame data.
+        """
+        request = requests_pb2.PrepareAndExecuteRequest()
+        request.connection_id = connection_id
+        request.statement_id = statement_id
+        request.sql = sql
+        if max_rows_total is not None:
+            request.max_rows_total = max_rows_total
+        if first_frame_max_size is not None:
+            request.first_frame_max_size = first_frame_max_size
+
+        response_data = self._apply(request, 'ExecuteResponse')
+        response = responses_pb2.ExecuteResponse()
+        response.ParseFromString(response_data)
+        return response.results
+
+    def prepare(self, connection_id, sql, max_rows_total=None):
+        """Prepares a statement.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param sql:
+            SQL query.
+
+        :param max_rows_total:
+            The maximum number of rows that will be allowed for this query.
+
+        :returns:
+            Signature of the prepared statement.
+        """
+        request = requests_pb2.PrepareRequest()
+        request.connection_id = connection_id
+        request.sql = sql
+        if max_rows_total is not None:
+            request.max_rows_total = max_rows_total
+
+        response_data = self._apply(request)
+        response = responses_pb2.PrepareResponse()
+        response.ParseFromString(response_data)
+        return response.statement
+
+    def execute(self, connection_id, statement_id, signature, 
parameter_values=None, first_frame_max_size=None):
+        """Returns a frame of rows.
+
+        The frame describes whether there may be another frame. If there is not
+        another frame, the current iteration is done when we have finished the
+        rows in the this frame.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param statement_id:
+            ID of the statement to fetch rows from.
+
+        :param signature:
+            common_pb2.Signature object
+
+        :param parameter_values:
+            A list of parameter values, if statement is to be executed; 
otherwise ``None``.
+
+        :param first_frame_max_size:
+            The maximum number of rows that will be returned in the first 
Frame returned for this query.
+
+        :returns:
+            Frame data, or ``None`` if there are no more.
+        """
+        request = requests_pb2.ExecuteRequest()
+        request.statementHandle.id = statement_id
+        request.statementHandle.connection_id = connection_id
+        request.statementHandle.signature.CopyFrom(signature)
+        if parameter_values is not None:
+            request.parameter_values.extend(parameter_values)
+            request.has_parameter_values = True
+        if first_frame_max_size is not None:
+            request.deprecated_first_frame_max_size = first_frame_max_size
+            request.first_frame_max_size = first_frame_max_size
+
+        response_data = self._apply(request)
+        response = responses_pb2.ExecuteResponse()
+        response.ParseFromString(response_data)
+        return response.results
+
+    def fetch(self, connection_id, statement_id, offset=0, 
frame_max_size=None):
+        """Returns a frame of rows.
+
+        The frame describes whether there may be another frame. If there is not
+        another frame, the current iteration is done when we have finished the
+        rows in the this frame.
+
+        :param connection_id:
+            ID of the current connection.
+
+        :param statement_id:
+            ID of the statement to fetch rows from.
+
+        :param offset:
+            Zero-based offset of first row in the requested frame.
+
+        :param frame_max_size:
+            Maximum number of rows to return; negative means no limit.
+
+        :returns:
+            Frame data, or ``None`` if there are no more.
+        """
+        request = requests_pb2.FetchRequest()
+        request.connection_id = connection_id
+        request.statement_id = statement_id
+        request.offset = offset
+        if frame_max_size is not None:
+            request.frame_max_size = frame_max_size
+
+        response_data = self._apply(request)
+        response = responses_pb2.FetchResponse()
+        response.ParseFromString(response_data)
+        return response.frame

http://git-wip-us.apache.org/repos/asf/phoenix/blob/14b57801/python/phoenixdb/phoenixdb/avatica/proto/__init__.py
----------------------------------------------------------------------
diff --git a/python/phoenixdb/phoenixdb/avatica/proto/__init__.py 
b/python/phoenixdb/phoenixdb/avatica/proto/__init__.py
new file mode 100644
index 0000000..e69de29

Reply via email to