Commit 9459d2a9 authored by Lukáš Lalinský's avatar Lukáš Lalinský

Python 3 support

parent dce64bc3
......@@ -4,8 +4,9 @@ Changelog
Version 0.5
-----------
- The driver now uses the Protocol Buffers protocol instead of JSON to communicate with Avatica.
- Dropped support for Phoenix 4.6 and older.
- Added support for Python 3.
- Switched from the JSON serialization to Protocol Buffers, improved compatibility with Phoenix 4.8.
- Phoenix 4.6 and older are no longer supported.
Version 0.4
-----------
......
#!/usr/bin/env bash
export CALCITE_VER=calcite-1.8.0
export CALCITE_DIR=calcite
rm -rf phoenixdb/$CALCITE_DIR
rm -rf phoenixdb/calcite
rm -rf calcite-tmp
git init calcite-tmp
......@@ -14,9 +13,10 @@ echo "avatica/core/src/main/protobuf/*" >> .git/info/sparse-checkout
git pull --depth=1 origin $CALCITE_VER
cd ..
mkdir -p phoenixdb/$CALCITE_DIR
protoc --proto_path=calcite-tmp/avatica/core/src/main/protobuf/ --python_out=phoenixdb/$CALCITE_DIR calcite-tmp/avatica/core/src/main/protobuf/*.proto
mkdir -p phoenixdb/calcite
protoc --proto_path=calcite-tmp/avatica/core/src/main/protobuf/ --python_out=phoenixdb/calcite calcite-tmp/avatica/core/src/main/protobuf/*.proto
sed -i 's/import common_pb2/from . import common_pb2/' phoenixdb/calcite/*_pb2.py
rm -rf calcite-tmp
echo '' >> phoenixdb/$CALCITE_DIR/__init__.py
echo '' >> phoenixdb/calcite/__init__.py
......@@ -16,16 +16,28 @@
import re
import socket
import httplib
import pprint
import math
import logging
import urlparse
import time
from HTMLParser import HTMLParser
from phoenixdb import errors
from phoenixdb.calcite import requests_pb2, common_pb2, responses_pb2
try:
import httplib
except ImportError:
import http.client as httplib
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__)
......@@ -191,7 +203,7 @@ class AvaticaClient(object):
if response.status != httplib.OK:
logger.debug("Received response\n%s", response_body)
if '<html>' in response_body:
if b'<html>' in response_body:
parse_error_page(response_body)
else:
# assume the response is in protobuf format
......@@ -201,7 +213,7 @@ class AvaticaClient(object):
message = common_pb2.WireMessage()
message.ParseFromString(response_body)
logger.debug("Received response\n%s", message.name)
logger.debug("Received response\n%s", message)
if expected_response_type is None:
expected_response_type = request_name.replace('Request', 'Response')
......@@ -380,6 +392,7 @@ class AvaticaClient(object):
response_data = self._apply(request, 'ExecuteResponse')
response = responses_pb2.ExecuteResponse()
response.ParseFromString(response_data)
logger.info('results %r', response.results)
return response.results
def prepare(self, connectionId, sql, maxRowCount=-1):
......
......@@ -13,7 +13,7 @@ from google.protobuf import descriptor_pb2
_sym_db = _symbol_database.Default()
import common_pb2 as common__pb2
from . import common_pb2 as common__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
......
......@@ -13,7 +13,7 @@ from google.protobuf import descriptor_pb2
_sym_db = _symbol_database.Default()
import common_pb2 as common__pb2
from . import common_pb2 as common__pb2
DESCRIPTOR = _descriptor.FileDescriptor(
......
......@@ -74,12 +74,14 @@ class Cursor(object):
def __iter__(self):
return self
def next(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.
......@@ -126,9 +128,6 @@ class Cursor(object):
self._id = id
def _set_signature(self, signature):
if signature is not None and signature.SerializeToString() == '':
signature = None
self._signature = signature
self._column_data_types = []
self._parameter_data_types = []
......@@ -144,9 +143,6 @@ class Cursor(object):
self._parameter_data_types.append(dtype)
def _set_frame(self, frame):
if frame is not None and frame.SerializeToString() == '':
frame = None
self._frame = frame
self._pos = None
......@@ -167,8 +163,8 @@ class Cursor(object):
result = results[0]
if result.own_statement:
self._set_id(result.statement_id)
self._set_signature(result.signature)
self._set_frame(result.first_frame)
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):
......
......@@ -18,18 +18,23 @@ __all__ = [
'ProgrammingError', 'NotSupportedError',
]
try:
_StandardError = StandardError
except NameError:
_StandardError = Exception
class Warning(StandardError):
class Warning(_StandardError):
"""Not used by this package, only defined for compatibility
with DB API 2.0."""
class Error(StandardError):
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)
super(_StandardError, self).__init__(message, code, sqlstate, cause)
@property
def message(self):
......
import unittest
import urlparse
from phoenixdb.avatica import parse_url
from phoenixdb.avatica import parse_url, urlparse
class ParseUrlTest(unittest.TestCase):
......
import dbapi20
import unittest
import phoenixdb
from . import dbapi20
from phoenixdb.tests import TEST_DB_URL
......
import sys
import unittest
import datetime
import phoenixdb
......@@ -266,23 +267,24 @@ class TypesTest(DatabaseTestCase):
self.createTable("phoenixdb_test_tbl1", "id integer primary key, val binary(2)")
with self.conn.cursor() as cursor:
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (1, 'ab')")
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (2, ?)", [phoenixdb.Binary('ab')])
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (2, ?)", [phoenixdb.Binary(b'ab')])
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (3, '\x01\x00')")
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (4, ?)", [phoenixdb.Binary('\x01\x00')])
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (4, ?)", [phoenixdb.Binary(b'\x01\x00')])
cursor.execute("SELECT id, val FROM phoenixdb_test_tbl1 ORDER BY id")
self.assertEqual(cursor.fetchall(), [
[1, 'ab'],
[2, 'ab'],
[3, '\x01\x00'],
[4, '\x01\x00'],
[1, b'ab'],
[2, b'ab'],
[3, b'\x01\x00'],
[4, b'\x01\x00'],
])
def test_binary_all_bytes(self):
self.createTable("phoenixdb_test_tbl1", "id integer primary key, val binary(256)")
with self.conn.cursor() as cursor:
value = ''
for i in range(256):
value += chr(i)
if sys.version_info[0] < 3:
value = ''.join(map(chr, range(256)))
else:
value = bytes(range(256))
cursor.execute("UPSERT INTO phoenixdb_test_tbl1 VALUES (1, ?)", [phoenixdb.Binary(value)])
cursor.execute("SELECT id, val FROM phoenixdb_test_tbl1 ORDER BY id")
self.assertEqual(cursor.fetchall(), [[1, value]])
......
......@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import time
import datetime
from decimal import Decimal
......@@ -56,7 +57,7 @@ def TimestampFromTicks(ticks):
def Binary(value):
"""Constructs an object capable of holding a binary (long) string value."""
return value
return bytes(value)
def time_from_java_sql_time(n):
......@@ -65,7 +66,7 @@ def time_from_java_sql_time(n):
def time_to_java_sql_time(t):
return ((t.hour * 60 + t.minute) * 60 + t.second) * 1000 + t.microsecond / 1000
return ((t.hour * 60 + t.minute) * 60 + t.second) * 1000 + t.microsecond // 1000
def date_from_java_sql_date(n):
......@@ -85,7 +86,7 @@ def datetime_from_java_sql_timestamp(n):
def datetime_to_java_sql_timestamp(d):
td = d - datetime.datetime(1970, 1, 1)
return td.microseconds / 1000 + (td.seconds + td.days * 24 * 3600) * 1000
return td.microseconds // 1000 + (td.seconds + td.days * 24 * 3600) * 1000
class ColumnType(object):
......@@ -94,6 +95,9 @@ class ColumnType(object):
self.eq_types = tuple(eq_types)
self.eq_types_set = set(eq_types)
def __eq__(self, other):
return other in self.eq_types_set
def __cmp__(self, other):
if other in self.eq_types_set:
return 0
......@@ -121,8 +125,8 @@ ROWID = ColumnType([])
BOOLEAN = ColumnType(['BOOLEAN'])
"""Type object that can be used to describe boolean columns. This is a phoenixdb-specific extension."""
# XXX ARRAY
# XXX ARRAY
JAVA_CLASSES = {
'bool_value': [
......@@ -136,7 +140,7 @@ JAVA_CLASSES = {
'number_value': [
('java.lang.Integer', common_pb2.INTEGER, None, int),
('java.lang.Short', common_pb2.SHORT, None, int),
('java.lang.Long', common_pb2.LONG, None, long),
('java.lang.Long', common_pb2.LONG, None, long if sys.version_info[0] < 3 else int),
('java.lang.Byte', common_pb2.BYTE, None, int),
('java.sql.Time', common_pb2.JAVA_SQL_TIME, time_to_java_sql_time, time_from_java_sql_time),
('java.sql.Date', common_pb2.JAVA_SQL_DATE, date_to_java_sql_date, date_from_java_sql_date),
......
-e git+https://bitbucket.org/lalinsky/python-sqlline.git#egg=sqlline
nose
protobuf>=3.0.0
......@@ -32,6 +32,14 @@ setup(
'release': ('setup.py', version),
},
},
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
],
install_requires=[
'protobuf>=3.0.0',
]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment