http://git-wip-us.apache.org/repos/asf/phoenix/blob/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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/3cac9217/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