You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ja...@apache.org on 2018/10/15 22:16:33 UTC
[08/22] phoenix git commit: PHOENIX-4688 Support SPNEGO for python
driver via requests-kerberos
http://git-wip-us.apache.org/repos/asf/phoenix/blob/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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/e62be9c8/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