From c8babc147f03f16e11f11ae6cbd822969bb53c18 Mon Sep 17 00:00:00 2001 From: Daniel Bento Date: Fri, 20 Sep 2013 18:11:41 +0100 Subject: [PATCH 01/62] kt_http.py crashes when we are getting an empty key value --- kyototycoon/kt_http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index afa8e0f..d42467a 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -848,4 +848,8 @@ def _pickle_packer(self, data): return pickle.dumps(data, self.pickle_protocol) def _pickle_unpacker(self, data): - return pickle.loads(data) + try: + res = pickle.loads(data) + except EOFError as err: + res = "" + return res From e4c7cbdfe34156868d091e770b3db91078e8b619 Mon Sep 17 00:00:00 2001 From: Daniel Bento Date: Tue, 10 Dec 2013 14:30:11 +0000 Subject: [PATCH 02/62] if we are setting a string, do not pack it as an object --- kyototycoon/kt_http.py | 7 +++++++ tests/t_utils.py | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index d42467a..f483410 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -845,6 +845,8 @@ def _rest_put(self, operation, key, value, expire): return res.status def _pickle_packer(self, data): + if type(data) is str: + return data return pickle.dumps(data, self.pickle_protocol) def _pickle_unpacker(self, data): @@ -852,4 +854,9 @@ def _pickle_unpacker(self, data): res = pickle.loads(data) except EOFError as err: res = "" + except pickle.UnpicklingError as err: + if type(data) is str: + res = data + else: + raise return res diff --git a/tests/t_utils.py b/tests/t_utils.py index e8a640f..7c1bbd0 100644 --- a/tests/t_utils.py +++ b/tests/t_utils.py @@ -14,11 +14,14 @@ def setUp(self): self.kt_core = kyototycoon.kt_http.ProtocolHandler() def test_packer(self): - str = 'hello world sir' - buf = self.kt_core._pickle_packer(str) - assert buf != str + stri = 'hello world sir' + buf = self.kt_core._pickle_packer(stri) + if type(stri) is str: + assert buf == stri + else: + assert buf != stri ret = self.kt_core._pickle_unpacker(buf) - self.assertEqual(str, ret) + self.assertEqual(stri, ret) num = 777 buf = self.kt_core._pickle_packer(num) From a6482c645b680fb1e5b8562f724295419a238123 Mon Sep 17 00:00:00 2001 From: Daniel Bento Date: Tue, 17 Dec 2013 12:18:32 +0000 Subject: [PATCH 03/62] fix EOFError --- kyototycoon/kt_http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index e3e3fd7..ec99190 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -873,6 +873,8 @@ def _pickle_unpacker(self, data): res = pickle.loads(data) except EOFError as err: res = "" + except ValueError as err: + res = data except pickle.UnpicklingError as err: if type(data) is str: res = data From 10a68ca7f35e32906eec2112aa586a74886e6bed Mon Sep 17 00:00:00 2001 From: Daniel Bento Date: Thu, 19 Dec 2013 16:30:48 +0000 Subject: [PATCH 04/62] add support for json packer/unpacker --- kyototycoon/kt_http.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index ec99190..869082c 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -22,6 +22,8 @@ except ImportError: import pickle +import json + # Stick with URL encoding for now. Eventually run a benchmark # to evaluate what the most approariate encoding algorithm is. KT_HTTP_HEADER = { @@ -328,6 +330,10 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.pack = self._pickle_packer self.unpack = self._pickle_unpacker + elif self.pack_type == KT_PACKER_JSON: + self.pack = self._json_packer + self.unpack = self._json_unpacker + elif self.pack_type == KT_PACKER_STRING: self.pack = lambda data: data self.unpack = lambda data: data @@ -881,3 +887,9 @@ def _pickle_unpacker(self, data): else: raise return res + + def _json_packer(self, data): + return json.dumps(data) + + def _json_unpacker(self, data): + return json.loads(data) From fdcc4bf7d1959c5d6810ef3888d2a4a8aae0189e Mon Sep 17 00:00:00 2001 From: hotdox Date: Mon, 6 May 2013 11:55:11 +0400 Subject: [PATCH 05/62] fix multidb handling --- kyototycoon/kt_http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 869082c..7ed6f90 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -394,8 +394,7 @@ def get(self, key, db=None): path = key if db: - path = '/%s/%s' % (db, key) - path = quote(path.encode('UTF-8')) + path = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) self.conn.request('GET', path) res, body = self.getresponse() @@ -526,8 +525,7 @@ def get_int(self, key, db=None): path = key if db: - path = '/%s/%s' % (db, key) - path = quote(path.encode('UTF-8')) + path = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) self.conn.request('GET', path) @@ -657,9 +655,8 @@ def add(self, key, value, expire, db): return False if db: - key = '/%s/%s' % (db, key) + key = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) - key = quote(key.encode('UTF-8')) value = self.pack(value) status = self._rest_put('add', key, value, expire) @@ -707,9 +704,8 @@ def remove(self, key, db): return False if db: - key = '/%s/%s' % (db, key) + key = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) - key = quote(key.encode('UTF-8')) self.conn.request('DELETE', key) res, body = self.getresponse() From 854ee0414c2d7461be08c04e3f20a5609d7148ce Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 28 Dec 2013 18:03:09 +0000 Subject: [PATCH 06/62] Minimal binary protocol support (get/get_bulk/set/set_bulk only). --- kyototycoon/kt_binary.py | 243 +++++++++++++++++++++++++++++++++++++ kyototycoon/kyototycoon.py | 6 +- 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 kyototycoon/kt_binary.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py new file mode 100644 index 0000000..252306f --- /dev/null +++ b/kyototycoon/kt_binary.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. +# +# This is based on Ulrich Mierendorff's code, originally at: +# - http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html +# + +import socket +import struct +import time +import kt_error + +try: + import cPickle as pickle +except ImportError: + import pickle + +import json + +KT_PACKER_CUSTOM = 0 +KT_PACKER_PICKLE = 1 +KT_PACKER_JSON = 2 +KT_PACKER_STRING = 3 + +MB_SET_BULK = 0xb8 +MB_GET_BULK = 0xba +MB_REMOVE_BULK = 0xb9 + +# Maximum signed 64bit integer... +DEFAULT_EXPIRE = 0x7fffffffffffffff + +class ProtocolHandler(object): + def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): + self.err = kt_error.KyotoTycoonError() + self.pickle_protocol = pickle_protocol + self.pack_type = pack_type + + if self.pack_type == KT_PACKER_PICKLE: + self.pack = self._pickle_packer + self.unpack = self._pickle_unpacker + + elif self.pack_type == KT_PACKER_JSON: + self.pack = self._json_packer + self.unpack = self._json_unpacker + + elif self.pack_type == KT_PACKER_STRING: + self.pack = lambda data: data + self.unpack = lambda data: data + + else: + raise Exception('unknown pack type specified') + + def error(self): + return self.err + + def cursor(self): + raise NotImplementedError + + def open(self, host, port, timeout): + self.socket = socket.create_connection((host, port), timeout) + return True + + def close(self): + self.socket.close() + return True + + def getresponse(self): + raise NotImplementedError + + def echo(self): + raise NotImplementedError + + def get(self, key, db=None): + if key is None: + return False + + values = self.get_bulk([key], True, db) + if not values: + return None + + return values[key] + + def set_bulk(self, kv_dict, expire, atomic, db): + if not isinstance(kv_dict, dict): + return False + + if len(kv_dict) < 1: + self.err.set_error(self.err.LOGIC) + return False + + if db is None: + db = 0 + + if expire is None: + expire = DEFAULT_EXPIRE + + request = [struct.pack('!BI', MB_SET_BULK, 0), struct.pack('!I', len(kv_dict))] + + for key, value in kv_dict.iteritems(): + value = self.pack(value) + request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) + request.append(key) + request.append(value) + + self._write(''.join(request)) + + magic, = struct.unpack('!B', self._read(1)) + if magic != MB_SET_BULK: + self.err.set_error(self.err.INTERNAL) + return False + + num_items, = struct.unpack('!I', self._read(4)) + + self.err.set_success() + return num_items + + def remove_bulk(self, keys, atomic, db): + raise NotImplementedError + + def get_bulk(self, keys, atomic, db): + if not hasattr(keys, '__iter__'): + self.err.set_error(self.err.LOGIC) + return None + + if db is None: + db = 0 + + request = [struct.pack('!BI', MB_GET_BULK, 0), struct.pack('!I', len(keys))] + + for key in keys: + request.append(struct.pack('!HI', db, len(key))) + request.append(key) + + self._write(''.join(request)) + + magic, = struct.unpack('!B', self._read(1)) + if magic != MB_GET_BULK: + self.err.set_error(self.err.INTERNAL) + return False + + num_items, = struct.unpack('!I', self._read(4)) + items = {} + for i in xrange(num_items): + key_db, key_length, value_length, key_expire = struct.unpack('!HIIq', self._read(18)) + key = self._read(key_length) + value = self._read(value_length) + items[key] = self.unpack(value) + return items + + def get_int(self, key, db=None): + raise NotImplementedError + + def vacuum(self, db): + raise NotImplementedError + + def match_prefix(self, prefix, max, db): + raise NotImplementedError + + def match_regex(self, regex, max, db): + raise NotImplementedError + + def set(self, key, value, expire, db): + return self.set_bulk({key: value}, expire, True, db) + + def add(self, key, value, expire, db): + raise NotImplementedError + + def cas(self, key, old_val, new_val, expire, db): + raise NotImplementedError + + def remove(self, key, db): + raise NotImplementedError + + def replace(self, key, value, expire, db): + raise NotImplementedError + + def append(self, key, value, expire, db): + raise NotImplementedError + + def increment(self, key, delta, expire, db): + raise NotImplementedError + + def increment_double(self, key, delta, expire, db): + raise NotImplementedError + + def report(self): + raise NotImplementedError + + def status(self, db=None): + raise NotImplementedError + + def clear(self, db=None): + raise NotImplementedError + + def count(self, db=None): + raise NotImplementedError + + def size(self, db=None): + raise NotImplementedError + + def _pickle_packer(self, data): + if type(data) is str: + return data + return pickle.dumps(data, self.pickle_protocol) + + def _pickle_unpacker(self, data): + try: + res = pickle.loads(data) + except EOFError as err: + res = "" + except ValueError as err: + res = data + except pickle.UnpicklingError as err: + if type(data) is str: + res = data + else: + raise + return res + + def _json_packer(self, data): + return json.dumps(data) + + def _json_unpacker(self, data): + return json.loads(data) + + def _write(self, data): + self.socket.sendall(data) + + def _read(self, bytecnt): + buf = [] + read = 0 + while read < bytecnt: + recv = self.socket.recv(bytecnt-read) + if recv: + buf.append(recv) + read += len(recv) + + return ''.join(buf) + diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index baf348e..c94c1aa 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -9,6 +9,7 @@ # standard: http://fallabs.com/kyototycoon/kyototycoon.idl import kt_http +import kt_binary KT_DEFAULT_HOST = '127.0.0.1' KT_DEFAULT_PORT = 1978 @@ -16,7 +17,10 @@ class KyotoTycoon(object): def __init__(self, binary=False, *args, **kwargs): - self.core = kt_http.ProtocolHandler(*args, **kwargs) + if binary: + self.core = kt_binary.ProtocolHandler(*args, **kwargs) + else: + self.core = kt_http.ProtocolHandler(*args, **kwargs) def error(self): return self.core.error() From 5cfccd8172fad355edf9b10d7669e5284f6d6f03 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 28 Dec 2013 18:28:04 +0000 Subject: [PATCH 07/62] Support remove/remove_bulk with the binary protocol, making the minimal set of read/insert/delete complete. --- kyototycoon/kt_binary.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 252306f..a6e06d2 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -119,7 +119,30 @@ def set_bulk(self, kv_dict, expire, atomic, db): return num_items def remove_bulk(self, keys, atomic, db): - raise NotImplementedError + if not hasattr(keys, '__iter__'): + self.err.set_error(self.err.LOGIC) + return 0 + + if db is None: + db = 0 + + request = [struct.pack('!BI', MB_REMOVE_BULK, 0), struct.pack('!I', len(keys))] + + for key in keys: + request.append(struct.pack('!HI', db, len(key))) + request.append(key) + + self._write(''.join(request)) + + magic, = struct.unpack('!B', self._read(1)) + if magic != MB_REMOVE_BULK: + self.err.set_error(self.err.INTERNAL) + return False + + num_items, = struct.unpack('!I', self._read(4)) + + self.err.set_success() + return num_items def get_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): @@ -173,7 +196,11 @@ def cas(self, key, old_val, new_val, expire, db): raise NotImplementedError def remove(self, key, db): - raise NotImplementedError + if key is None: + self.err.set_error(self.err.LOGIC) + return False + + return self.remove_bulk([key], True, db) def replace(self, key, value, expire, db): raise NotImplementedError From bd667718502f05ae386662082bbc7591d968f157 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 28 Dec 2013 18:43:29 +0000 Subject: [PATCH 08/62] Updated README with binary protocol support information. --- README | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README b/README index 68fb5c7..f41bcde 100644 --- a/README +++ b/README @@ -17,6 +17,20 @@ This fork is backwards compatible, and includes cursor support and significant performance improvements over the original library. +The binary protocol is also supported along the HTTP protocol, +but for a minimal subset of operations only, namely: + + - get() and get_bulk() + - set() and set_bulk() + - remove() and remove_bulk() + +These methods are based on Ulrich Mierendorff's code with only +minimal changes to make it fit this library. You can find the +original library supporting the binary protocol at his website. + +http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html + + INSTALLATION ------------ Using pip:: From 75cb2e71ab3e27d0c27c7e6dde68082e2fe4fa6e Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 29 Dec 2013 13:47:03 +0000 Subject: [PATCH 09/62] Removed two not implemented methods that were only actually used by the HTTP transport. --- kyototycoon/kt_binary.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index a6e06d2..35464ff 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -68,12 +68,6 @@ def close(self): self.socket.close() return True - def getresponse(self): - raise NotImplementedError - - def echo(self): - raise NotImplementedError - def get(self, key, db=None): if key is None: return False From ad659cad5e34dc64e5941514b7b803bb8d7256fd Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 29 Dec 2013 14:01:14 +0000 Subject: [PATCH 10/62] Removed a useless time import, also left over from the HTTP transport implementation. --- kyototycoon/kt_binary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 35464ff..379b873 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -11,7 +11,6 @@ import socket import struct -import time import kt_error try: From 69ba2d693ec552640a190023c5cfed5754fb623e Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Mon, 30 Dec 2013 16:43:24 +0000 Subject: [PATCH 11/62] Fixed exception with unicode keys in the binary protocol. --- kyototycoon/kt_binary.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 379b873..c2c8dca 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -94,7 +94,13 @@ def set_bulk(self, kv_dict, expire, atomic, db): request = [struct.pack('!BI', MB_SET_BULK, 0), struct.pack('!I', len(kv_dict))] for key, value in kv_dict.iteritems(): + key = key.encode("ascii") value = self.pack(value) + + # For consistency with the HTTP implementation's error behavior... + if isinstance(value, unicode): + value = value.encode("ascii") + request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) request.append(key) request.append(value) @@ -122,6 +128,7 @@ def remove_bulk(self, keys, atomic, db): request = [struct.pack('!BI', MB_REMOVE_BULK, 0), struct.pack('!I', len(keys))] for key in keys: + key = key.encode("ascii") request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -148,6 +155,7 @@ def get_bulk(self, keys, atomic, db): request = [struct.pack('!BI', MB_GET_BULK, 0), struct.pack('!I', len(keys))] for key in keys: + key = key.encode("ascii") request.append(struct.pack('!HI', db, len(key))) request.append(key) From fab50ccc079ce8bb84fd4a3df822d4948c8c1f3a Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Mon, 30 Dec 2013 23:49:39 +0000 Subject: [PATCH 12/62] Proper handling of closed sockets. (Fixes CPU hogging on socket closed py peer.) --- kyototycoon/kt_binary.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index c2c8dca..a2f35ac 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -37,6 +37,7 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.err = kt_error.KyotoTycoonError() self.pickle_protocol = pickle_protocol self.pack_type = pack_type + self.socket = None if self.pack_type == KT_PACKER_PICKLE: self.pack = self._pickle_packer @@ -64,6 +65,7 @@ def open(self, host, port, timeout): return True def close(self): + self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() return True @@ -262,10 +264,12 @@ def _read(self, bytecnt): buf = [] read = 0 while read < bytecnt: - recv = self.socket.recv(bytecnt-read) - if recv: - buf.append(recv) - read += len(recv) + recv = self.socket.recv(bytecnt - read) + if not recv: + raise IOError("no data while reading") + + buf.append(recv) + read += len(recv) return ''.join(buf) From a7e882433cff53f46eccd636f16ccbcc38b17be9 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 12 Jan 2014 18:32:49 +0000 Subject: [PATCH 13/62] Small performance improvements. Use 'python-cjson' for extra performance, if installed. --- kyototycoon/kt_binary.py | 26 ++++++++++++++++---------- kyototycoon/kt_http.py | 21 ++++++++++++++------- kyototycoon/kyototycoon.py | 4 ++-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index a2f35ac..89a6c37 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -11,14 +11,20 @@ import socket import struct -import kt_error + +from . import kt_error try: import cPickle as pickle except ImportError: import pickle -import json +try: + import cjson as json + json.dumps = json.encode + json.loads = json.decode +except ImportError: + import json KT_PACKER_CUSTOM = 0 KT_PACKER_PICKLE = 1 @@ -93,15 +99,15 @@ def set_bulk(self, kv_dict, expire, atomic, db): if expire is None: expire = DEFAULT_EXPIRE - request = [struct.pack('!BI', MB_SET_BULK, 0), struct.pack('!I', len(kv_dict))] + request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] for key, value in kv_dict.iteritems(): - key = key.encode("ascii") + key = key.encode('ascii') value = self.pack(value) # For consistency with the HTTP implementation's error behavior... - if isinstance(value, unicode): - value = value.encode("ascii") + if isinstance(value, type(u'')): + value = value.encode('ascii') request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) request.append(key) @@ -127,10 +133,10 @@ def remove_bulk(self, keys, atomic, db): if db is None: db = 0 - request = [struct.pack('!BI', MB_REMOVE_BULK, 0), struct.pack('!I', len(keys))] + request = [struct.pack('!BII', MB_REMOVE_BULK, 0, len(keys))] for key in keys: - key = key.encode("ascii") + key = key.encode('ascii') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -154,10 +160,10 @@ def get_bulk(self, keys, atomic, db): if db is None: db = 0 - request = [struct.pack('!BI', MB_GET_BULK, 0), struct.pack('!I', len(keys))] + request = [struct.pack('!BII', MB_GET_BULK, 0, len(keys))] for key in keys: - key = key.encode("ascii") + key = key.encode('ascii') request.append(struct.pack('!HI', db, len(key))) request.append(key) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 7ed6f90..fbb5b1c 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -9,7 +9,9 @@ import httplib import struct import time -import kt_error + +from . import kt_error + try: from percentcoding import quote, unquote except ImportError: @@ -22,7 +24,12 @@ except ImportError: import pickle -import json +try: + import cjson as json + json.dumps = json.encode + json.loads = json.decode +except ImportError: + import json # Stick with URL encoding for now. Eventually run a benchmark # to evaluate what the most approariate encoding algorithm is. @@ -36,7 +43,7 @@ KT_PACKER_STRING = 3 def _dict_to_tsv(dict): - return '\n'.join(quote(k) + '\t' + quote(str(v)) for (k, v) in dict.items()) + return '\n'.join(quote(k) + '\t' + quote(str(v)) for (k, v) in dict.iteritems()) def _content_type_decoder(content_type=''): ''' Select the appropriate decoding function to use based on the response headers. ''' @@ -356,14 +363,14 @@ def open(self, host, port, timeout): try: self.conn = httplib.HTTPConnection(host, port, timeout=timeout) - except Exception, e: + except Exception as e: raise e return True def close(self): try: self.conn.close() - except Exception, e: + except Exception as e: raise e return True @@ -427,7 +434,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): if atomic: request_body = 'atomic\t\n' - for k, v in kv_dict.items(): + for k, v in kv_dict.iteritems(): k = quote(k) v = quote(self.pack(v)) request_body += '_' + k + '\t' + v + '\n' @@ -511,7 +518,7 @@ def get_bulk(self, keys, atomic, db): self.err.set_error(self.err.NOTFOUND) return {} - for k, v in res_dict.items(): + for k, v in res_dict.iteritems(): if v is not None: rv[k[1:]] = self.unpack(v) diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index c94c1aa..a8c827e 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -8,8 +8,8 @@ # Note that python-kyototycoon follows the following interface # standard: http://fallabs.com/kyototycoon/kyototycoon.idl -import kt_http -import kt_binary +from . import kt_http +from . import kt_binary KT_DEFAULT_HOST = '127.0.0.1' KT_DEFAULT_PORT = 1978 From db534121da007eb1504dcf38d9701482d3876406 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 12 Jan 2014 19:27:15 +0000 Subject: [PATCH 14/62] Added a script to benchmark the library with several parameters. --- README | 13 +++- tests/kt_performance.py | 128 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/kt_performance.py diff --git a/README b/README index f41bcde..12e3986 100644 --- a/README +++ b/README @@ -18,7 +18,8 @@ and significant performance improvements over the original library. The binary protocol is also supported along the HTTP protocol, -but for a minimal subset of operations only, namely: +for a performance improvement of around 6x. Only a minimal +subset of operations are supported for this protocol, namely: - get() and get_bulk() - set() and set_bulk() @@ -43,6 +44,16 @@ Or, from source:: sudo python setup.py install +KNOWN ISSUES +------------ + * When using the HTTP protocol and KT_PACKER_STRING, if + values are unicode strings "set()" is about 20x slower + compared to non-unicode strings. + * With Python 2.6, all HTTP operations are about 20x + slower than Python 2.7. There is no such difference + when using the binary protocol. + + AUTHORS ------- * Toru Maesaka diff --git a/tests/kt_performance.py b/tests/kt_performance.py new file mode 100644 index 0000000..619d101 --- /dev/null +++ b/tests/kt_performance.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# kt_performance.py - measure performance of the python-kyototycoon library. +# +# Copyright (c) 2014 Carlos Rodrigues +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +from __future__ import print_function +from __future__ import division + +import sys +import os, os.path + +from time import time +from getopt import getopt, GetoptError + +from kyototycoon import KyotoTycoon + + +NUM_ITERATIONS = 2000 + + +def print_usage(): + """Output the proper usage syntax for this program.""" + + print("USAGE: %s [-s ]" % os.path.basename(sys.argv[0])) + + +def parse_args(): + """Parse and enforce command-line arguments.""" + + try: + options, _ = getopt(sys.argv[1:], "s:", ["server="]) + except GetoptError as e: + print("error: %s." % e, file=sys.stderr) + print_usage() + sys.exit(1) + + server = { "host": "127.0.0.1", "port": 1978 } + + for option, value in options: + if option in ("-s", "--server"): + fields = value.strip().split(":") + server["host"] = fields[0].strip() + + if len(fields) > 1: + server["port"] = int(fields[1]) + + return (server,) + + +def main(): + server, = parse_args() + + print("Running %d iterations for each parameter..." % NUM_ITERATIONS) + + header = "%-15s | %-15s | %-16s | %-7s | %-14s | %-6s" % \ + ("Binary Protocol", "Unicode Strings", "Packer Type", "Elapsed", + "Iteration Rate", "Passed") + print(header) + print("=" * len(header)) + + for binary in (True, False): + for is_unicode in (False, True): + for packer_type, packer_name in ((1, "KT_PACKER_PICKLE"), + (2, "KT_PACKER_JSON"), + (3, "KT_PACKER_STRING")): + kt = KyotoTycoon(binary=binary, pack_type=packer_type) + kt.open(server["host"], server["port"], timeout=2) + + start = time() + bad = 0 + + for i in xrange(NUM_ITERATIONS): + if is_unicode: + key = u"key-u-%d-%d" % (packer_type, i) + value = u"value-u-%d-%d-%.0f" % (packer_type, i, start) + else: + key = "key-s-%d-%d" % (packer_type, i) + value = "value-s-%d-%d-%.0f" % (packer_type, i, start) + + kt.set(key, value) + output = kt.get(key) + + if output != value: + bad += 1 + + kt.remove(key) + output = kt.get(key) + + if output is not None: + bad += 1 + + duration = time() - start + rate = NUM_ITERATIONS / duration + + print("%-15s | %-15s | %-16s | %5.2f s | %10.2f ips | %-6s" % + (binary, is_unicode, packer_name, duration, rate, + "No" if bad > 0 else "Yes")) + + kt.close() + + +if __name__ == "__main__": + main() + + +# vim: set expandtab ts=4 sw=4 From 666df250fdf0780f2aff6e66cd26a911c8cf76c3 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 12 Jan 2014 19:53:46 +0000 Subject: [PATCH 15/62] Altered the README for markdown syntax, plus a few content changes. --- README | 60 ----------------------------------------------- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 60 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 12e3986..0000000 --- a/README +++ /dev/null @@ -1,60 +0,0 @@ -ABOUT ------ -Python client library for Kyoto Tycoon. For more information on -Kyoto Tycoon please refer to the official project website. - - http://fallabs.com/kyototycoon/ - -python-kyototycoon's interface follows the preferred interface -provided by Kyoto Tycoon's original author(s). - - http://fallabs.com/kyototycoon/kyototycoon.idl - -There is currently no documentation for this software but for -the meantime please see kyototycoon.py for the interface. - -This fork is backwards compatible, and includes cursor support -and significant performance improvements over the original -library. - -The binary protocol is also supported along the HTTP protocol, -for a performance improvement of around 6x. Only a minimal -subset of operations are supported for this protocol, namely: - - - get() and get_bulk() - - set() and set_bulk() - - remove() and remove_bulk() - -These methods are based on Ulrich Mierendorff's code with only -minimal changes to make it fit this library. You can find the -original library supporting the binary protocol at his website. - -http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html - - -INSTALLATION ------------- -Using pip:: - - pip install python-kyototycoon - -Or, from source:: - - python setup.py build - sudo python setup.py install - - -KNOWN ISSUES ------------- - * When using the HTTP protocol and KT_PACKER_STRING, if - values are unicode strings "set()" is about 20x slower - compared to non-unicode strings. - * With Python 2.6, all HTTP operations are about 20x - slower than Python 2.7. There is no such difference - when using the binary protocol. - - -AUTHORS -------- -* Toru Maesaka -* Stephen Hamer diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c502e6 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +About +----- + +Python client library for Kyoto Tycoon. For more information on +Kyoto Tycoon please refer to the official project website: + + * http://fallabs.com/kyototycoon/ + +This library's interface follows the preferred interface +provided by Kyoto Tycoon's original author(s): + + * http://fallabs.com/kyototycoon/kyototycoon.idl + +There is currently no documentation for this software but in +the meantime please see "kyototycoon.py" for the interface. + +This fork is backwards compatible, and includes cursor support +and significant performance improvements over the original +library. + +The more efficient binary protocol is also supported along with +the HTTP protocol, for a performance improvement of around **6x**. +Only a minimal subset of operations are supported for this +protocol, namely: + + * "get()" and "get_bulk()" + * "set()" and "set_bulk()" + * "remove()" and "remove_bulk()" + +Currently, the "play_script()" operation is not implemented for +either protocol choices. + +Installation +------------ + +Using pip: + + pip install python-kyototycoon + +Or, from source: + + python setup.py build + sudo python setup.py install + + +Known Issues +------------ + + * When using the HTTP protocol and "KT_PACKER_STRING", if + values are unicode strings then the "set()" method is + about **20x** slower when compared to non-unicode strings. + + * With Python 2.6, all HTTP operations are about **20x** + slower than Python 2.7 (tested on a Debian machine). + There is no such performance difference between 2.6/2.7 + when using the binary protocol. + + +Authors and Thanks +------------------ + + * Toru Maesaka + * Stephen Hamer + +Binary protocol support was added by Carlos Rodrigues based +on Ulrich Mierendorff's code with only minimal changes to make +it fit this library. You can find the original library at the +following URL: + + * http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html From a3de64c013748cfbe632e1e8a8b1efae55a0c5e3 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 12 Jan 2014 20:28:10 +0000 Subject: [PATCH 16/62] Bump version. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f05b7ee..94bc8f5 100644 --- a/setup.py +++ b/setup.py @@ -14,11 +14,11 @@ maintainer_email='stephen.hamer@upverter.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.4.8', + version='0.5.1', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], - requires=['percentcoding'], + requires=['percentcoding', 'cjson'], url='https://github.com/upverter/python-kyototycoon', zip_safe=False ) From 7a6bc26d13d4bbe12fadd0505bbfaace1000947a Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 14 Jan 2014 22:55:00 +0000 Subject: [PATCH 17/62] Reverted README to reStructuredText, needed for PyPI. --- README | 54 ++++++++++++++++++++++++++++++++++++++++++ README.md | 70 ------------------------------------------------------- 2 files changed, 54 insertions(+), 70 deletions(-) create mode 100644 README delete mode 100644 README.md diff --git a/README b/README new file mode 100644 index 0000000..4f6baa6 --- /dev/null +++ b/README @@ -0,0 +1,54 @@ +ABOUT +----- +Python client library for Kyoto Tycoon. For more information on +Kyoto Tycoon please refer to the official project website: + + http://fallabs.com/kyototycoon/ + +This library's interface follows the preferred interface +provided by Kyoto Tycoon's original author(s): + + http://fallabs.com/kyototycoon/kyototycoon.idl + +There is currently no documentation for this software but in +the meantime please see the file "kyototycoon.py" for the +interface. + +This fork is backwards compatible, and includes cursor support +and significant performance improvements over the original +library. + +The more efficient binary protocol is also supported along with +the HTTP protocol. It provides a performance improvement of up +to 6x, but only the following operations are available: + + * ``get()`` and ``get_bulk()`` + * ``set()`` and ``set_bulk()`` + * ``remove()`` and ``remove_bulk()`` + +Other operations will throw a ``NotImplementedError`` exception. + + +INSTALLATION +------------ +Using pip:: + + pip install python-kyototycoon + +Or, from source:: + + python setup.py build + sudo python setup.py install + + +AUTHORS +------- + * Toru Maesaka + * Stephen Hamer + +Binary protocol support was added by Carlos Rodrigues based +on Ulrich Mierendorff's code with only minimal changes to make +it fit this library. You can find the original library at the +following URL: + + http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html diff --git a/README.md b/README.md deleted file mode 100644 index 2c502e6..0000000 --- a/README.md +++ /dev/null @@ -1,70 +0,0 @@ -About ------ - -Python client library for Kyoto Tycoon. For more information on -Kyoto Tycoon please refer to the official project website: - - * http://fallabs.com/kyototycoon/ - -This library's interface follows the preferred interface -provided by Kyoto Tycoon's original author(s): - - * http://fallabs.com/kyototycoon/kyototycoon.idl - -There is currently no documentation for this software but in -the meantime please see "kyototycoon.py" for the interface. - -This fork is backwards compatible, and includes cursor support -and significant performance improvements over the original -library. - -The more efficient binary protocol is also supported along with -the HTTP protocol, for a performance improvement of around **6x**. -Only a minimal subset of operations are supported for this -protocol, namely: - - * "get()" and "get_bulk()" - * "set()" and "set_bulk()" - * "remove()" and "remove_bulk()" - -Currently, the "play_script()" operation is not implemented for -either protocol choices. - -Installation ------------- - -Using pip: - - pip install python-kyototycoon - -Or, from source: - - python setup.py build - sudo python setup.py install - - -Known Issues ------------- - - * When using the HTTP protocol and "KT_PACKER_STRING", if - values are unicode strings then the "set()" method is - about **20x** slower when compared to non-unicode strings. - - * With Python 2.6, all HTTP operations are about **20x** - slower than Python 2.7 (tested on a Debian machine). - There is no such performance difference between 2.6/2.7 - when using the binary protocol. - - -Authors and Thanks ------------------- - - * Toru Maesaka - * Stephen Hamer - -Binary protocol support was added by Carlos Rodrigues based -on Ulrich Mierendorff's code with only minimal changes to make -it fit this library. You can find the original library at the -following URL: - - * http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html From cfdbf1f2e28f3918f118036cb4927c2c64ca5844 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 14 Jan 2014 23:12:54 +0000 Subject: [PATCH 18/62] Modify shebang line in the performance script to support virtualenv. --- tests/kt_performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/kt_performance.py b/tests/kt_performance.py index 619d101..d98c4d3 100644 --- a/tests/kt_performance.py +++ b/tests/kt_performance.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # # kt_performance.py - measure performance of the python-kyototycoon library. From bf72f66c54798372d6000aacde1b48190f2b38b9 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Thu, 16 Jan 2014 23:27:37 +0000 Subject: [PATCH 19/62] Replace json for simplejson, it is better supported and seemingly as fast. --- kyototycoon/kt_binary.py | 4 +--- kyototycoon/kt_http.py | 4 +--- setup.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 89a6c37..22298fc 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -20,9 +20,7 @@ import pickle try: - import cjson as json - json.dumps = json.encode - json.loads = json.decode + import simplejson as json except ImportError: import json diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index fbb5b1c..bf4e399 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -25,9 +25,7 @@ import pickle try: - import cjson as json - json.dumps = json.encode - json.loads = json.decode + import simplejson as json except ImportError: import json diff --git a/setup.py b/setup.py index 94bc8f5..b41af46 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], - requires=['percentcoding', 'cjson'], + requires=['percentcoding', 'simplejson'], url='https://github.com/upverter/python-kyototycoon', zip_safe=False ) From 85dd2888ca840f300a8204f92d00f7d6e155a8db Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 18:01:55 +0000 Subject: [PATCH 20/62] Python 3 support. Better handling of unicode keys and values with non-ASCII characters. Fix set() slowness over HTTP when using KT_PACKER_STRING and unicode values. Match binary protocol's set()/get() return values with the HTTP protocol (boolean instead of int). --- kyototycoon/__init__.py | 12 +++++- kyototycoon/kt_binary.py | 83 +++++++++++++------------------------ kyototycoon/kt_error.py | 3 ++ kyototycoon/kt_http.py | 89 +++++++++++++++++----------------------- tests/kt_performance.py | 68 ++++++++++++++++-------------- 5 files changed, 117 insertions(+), 138 deletions(-) diff --git a/kyototycoon/__init__.py b/kyototycoon/__init__.py index 85b1549..5731f2c 100644 --- a/kyototycoon/__init__.py +++ b/kyototycoon/__init__.py @@ -1 +1,11 @@ -from kyototycoon import * +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +try: + # For Python 3.x... + from kyototycoon.kyototycoon import KyotoTycoon +except ImportError: + # For Python 2.x... + from kyototycoon import * + +# vim: set expandtab ts=4 sw=4 diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 22298fc..efd8fda 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -1,6 +1,7 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # -# Copyright 2011, Toru Maesaka +# Copyright 2013, Carlos Rodrigues # # Redistribution and use of this source code is licensed under # the BSD license. See COPYING file for license description. @@ -39,24 +40,22 @@ class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.err = kt_error.KyotoTycoonError() - self.pickle_protocol = pickle_protocol - self.pack_type = pack_type self.socket = None - if self.pack_type == KT_PACKER_PICKLE: - self.pack = self._pickle_packer - self.unpack = self._pickle_unpacker + if pack_type == KT_PACKER_PICKLE: + self.pack = lambda data: pickle.dumps(data, pickle_protocol) + self.unpack = lambda data: pickle.loads(data) - elif self.pack_type == KT_PACKER_JSON: - self.pack = self._json_packer - self.unpack = self._json_unpacker + elif pack_type == KT_PACKER_JSON: + self.pack = lambda data: json.dumps(data).encode('UTF-8') + self.unpack = lambda data: json.loads(data.decode('UTF-8')) - elif self.pack_type == KT_PACKER_STRING: - self.pack = lambda data: data - self.unpack = lambda data: data + elif pack_type == KT_PACKER_STRING: + self.pack = lambda data: data.encode('UTF-8') + self.unpack = lambda data: data.decode('UTF-8') else: - raise Exception('unknown pack type specified') + raise Exception('unsupported pack type specified') def error(self): return self.err @@ -99,19 +98,15 @@ def set_bulk(self, kv_dict, expire, atomic, db): request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] - for key, value in kv_dict.iteritems(): - key = key.encode('ascii') + for key, value in kv_dict.items(): + key = key.encode('UTF-8') value = self.pack(value) - # For consistency with the HTTP implementation's error behavior... - if isinstance(value, type(u'')): - value = value.encode('ascii') - request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) request.append(key) request.append(value) - self._write(''.join(request)) + self._write(b''.join(request)) magic, = struct.unpack('!B', self._read(1)) if magic != MB_SET_BULK: @@ -134,11 +129,11 @@ def remove_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_REMOVE_BULK, 0, len(keys))] for key in keys: - key = key.encode('ascii') + key = key.encode('UTF-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) - self._write(''.join(request)) + self._write(b''.join(request)) magic, = struct.unpack('!B', self._read(1)) if magic != MB_REMOVE_BULK: @@ -161,11 +156,11 @@ def get_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_GET_BULK, 0, len(keys))] for key in keys: - key = key.encode('ascii') + key = key.encode('UTF-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) - self._write(''.join(request)) + self._write(b''.join(request)) magic, = struct.unpack('!B', self._read(1)) if magic != MB_GET_BULK: @@ -174,11 +169,11 @@ def get_bulk(self, keys, atomic, db): num_items, = struct.unpack('!I', self._read(4)) items = {} - for i in xrange(num_items): + for i in range(num_items): key_db, key_length, value_length, key_expire = struct.unpack('!HIIq', self._read(18)) key = self._read(key_length) value = self._read(value_length) - items[key] = self.unpack(value) + items[key.decode('UTF-8')] = self.unpack(value) return items def get_int(self, key, db=None): @@ -194,7 +189,8 @@ def match_regex(self, regex, max, db): raise NotImplementedError def set(self, key, value, expire, db): - return self.set_bulk({key: value}, expire, True, db) + numitems = self.set_bulk({key: value}, expire, True, db) + return numitems > 0 def add(self, key, value, expire, db): raise NotImplementedError @@ -207,7 +203,8 @@ def remove(self, key, db): self.err.set_error(self.err.LOGIC) return False - return self.remove_bulk([key], True, db) + numitems = self.remove_bulk([key], True, db) + return numitems > 0 def replace(self, key, value, expire, db): raise NotImplementedError @@ -236,31 +233,6 @@ def count(self, db=None): def size(self, db=None): raise NotImplementedError - def _pickle_packer(self, data): - if type(data) is str: - return data - return pickle.dumps(data, self.pickle_protocol) - - def _pickle_unpacker(self, data): - try: - res = pickle.loads(data) - except EOFError as err: - res = "" - except ValueError as err: - res = data - except pickle.UnpicklingError as err: - if type(data) is str: - res = data - else: - raise - return res - - def _json_packer(self, data): - return json.dumps(data) - - def _json_unpacker(self, data): - return json.loads(data) - def _write(self, data): self.socket.sendall(data) @@ -270,10 +242,11 @@ def _read(self, bytecnt): while read < bytecnt: recv = self.socket.recv(bytecnt - read) if not recv: - raise IOError("no data while reading") + raise IOError('no data while reading') buf.append(recv) read += len(recv) - return ''.join(buf) + return b''.join(buf) +# vim: set expandtab ts=4 sw=4 diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index b9d102e..b09fe81 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka # @@ -58,3 +59,5 @@ def name(self): def message(self): return self.error_message + +# vim: set expandtab ts=4 sw=4 diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index bf4e399..6f0d957 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka # @@ -6,18 +7,27 @@ # the BSD license. See COPYING file for license description. import base64 -import httplib import struct import time from . import kt_error +try: + import httplib +except ImportError: + import http.client as httplib + try: from percentcoding import quote, unquote except ImportError: - from urllib import quote as _quote - from urllib import unquote - quote = lambda s: _quote(s, safe="") + try: + from urllib import quote as _quote + from urllib import unquote + except ImportError: + from urllib.parse import quote as _quote + from urllib.parse import unquote + + quote = lambda s: _quote(s, safe='') try: import cPickle as pickle @@ -41,7 +51,7 @@ KT_PACKER_STRING = 3 def _dict_to_tsv(dict): - return '\n'.join(quote(k) + '\t' + quote(str(v)) for (k, v) in dict.iteritems()) + return '\n'.join(quote(k) + '\t' + quote(str(v)) for (k, v) in dict.items()) def _content_type_decoder(content_type=''): ''' Select the appropriate decoding function to use based on the response headers. ''' @@ -328,23 +338,21 @@ def delete(self): class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.err = kt_error.KyotoTycoonError() - self.pickle_protocol = pickle_protocol - self.pack_type = pack_type - if self.pack_type == KT_PACKER_PICKLE: - self.pack = self._pickle_packer - self.unpack = self._pickle_unpacker + if pack_type == KT_PACKER_PICKLE: + self.pack = lambda data: pickle.dumps(data, pickle_protocol) + self.unpack = lambda data: pickle.loads(data) - elif self.pack_type == KT_PACKER_JSON: - self.pack = self._json_packer - self.unpack = self._json_unpacker + elif pack_type == KT_PACKER_JSON: + self.pack = lambda data: json.dumps(data).encode('UTF-8') + self.unpack = lambda data: json.loads(data.decode('UTF-8')) - elif self.pack_type == KT_PACKER_STRING: - self.pack = lambda data: data - self.unpack = lambda data: data + elif pack_type == KT_PACKER_STRING: + self.pack = lambda data: data.encode('UTF-8') + self.unpack = lambda data: data.decode('UTF-8') else: - raise Exception('unknown pack type specified') + raise Exception('unsupported pack type specified') def error(self): return self.err @@ -397,11 +405,11 @@ def get(self, key, db=None): if key is None: return False - path = key + key = quote(key.encode('UTF-8')) if db: - path = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) - self.conn.request('GET', path) + self.conn.request('GET', key) res, body = self.getresponse() if res.status == 404: @@ -432,7 +440,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): if atomic: request_body = 'atomic\t\n' - for k, v in kv_dict.iteritems(): + for k, v in kv_dict.items(): k = quote(k) v = quote(self.pack(v)) request_body += '_' + k + '\t' + v + '\n' @@ -516,7 +524,7 @@ def get_bulk(self, keys, atomic, db): self.err.set_error(self.err.NOTFOUND) return {} - for k, v in res_dict.iteritems(): + for k, v in res_dict.items(): if v is not None: rv[k[1:]] = self.unpack(v) @@ -528,11 +536,11 @@ def get_int(self, key, db=None): self.err.set_error(self.err.LOGIC) return False - path = key + key = quote(key.encode('UTF-8')) if db: - path = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) - self.conn.request('GET', path) + self.conn.request('GET', key) res, body = self.getresponse() if res.status != 200: @@ -659,8 +667,9 @@ def add(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False + key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) value = self.pack(value) status = self._rest_put('add', key, value, expire) @@ -708,8 +717,9 @@ def remove(self, key, db): self.err.set_error(self.err.LOGIC) return False + key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), quote(key).encode('UTF-8')) + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) self.conn.request('DELETE', key) @@ -870,27 +880,4 @@ def _rest_put(self, operation, key, value, expire): res, body = self.getresponse() return res.status - def _pickle_packer(self, data): - if type(data) is str: - return data - return pickle.dumps(data, self.pickle_protocol) - - def _pickle_unpacker(self, data): - try: - res = pickle.loads(data) - except EOFError as err: - res = "" - except ValueError as err: - res = data - except pickle.UnpicklingError as err: - if type(data) is str: - res = data - else: - raise - return res - - def _json_packer(self, data): - return json.dumps(data) - - def _json_unpacker(self, data): - return json.loads(data) +# vim: set expandtab ts=4 sw=4 diff --git a/tests/kt_performance.py b/tests/kt_performance.py index d98c4d3..f59baeb 100644 --- a/tests/kt_performance.py +++ b/tests/kt_performance.py @@ -28,6 +28,9 @@ from __future__ import print_function from __future__ import division +#from __future__ import unicode_literals + + import sys import os, os.path @@ -74,51 +77,54 @@ def main(): print("Running %d iterations for each parameter..." % NUM_ITERATIONS) - header = "%-15s | %-15s | %-16s | %-7s | %-14s | %-6s" % \ - ("Binary Protocol", "Unicode Strings", "Packer Type", "Elapsed", + header = "%-15s | %-16s | %-16s | %-7s | %-14s | %-6s" % \ + ("Binary Protocol", "Unicode Literals", "Packer Type", "Elapsed", "Iteration Rate", "Passed") print(header) print("=" * len(header)) + # Test for both Python 2.x and 3.x... + unicode_literal = str("") if sys.version_info[0] == 3 else unicode("") + is_unicode = isinstance("", type(unicode_literal)) + for binary in (True, False): - for is_unicode in (False, True): - for packer_type, packer_name in ((1, "KT_PACKER_PICKLE"), - (2, "KT_PACKER_JSON"), - (3, "KT_PACKER_STRING")): - kt = KyotoTycoon(binary=binary, pack_type=packer_type) - kt.open(server["host"], server["port"], timeout=2) + for packer_type, packer_name in ((1, "KT_PACKER_PICKLE"), + (2, "KT_PACKER_JSON"), + (3, "KT_PACKER_STRING")): + kt = KyotoTycoon(binary=binary, pack_type=packer_type) + kt.open(server["host"], server["port"], timeout=2) - start = time() - bad = 0 + start = time() + bad = 0 - for i in xrange(NUM_ITERATIONS): - if is_unicode: - key = u"key-u-%d-%d" % (packer_type, i) - value = u"value-u-%d-%d-%.0f" % (packer_type, i, start) - else: - key = "key-s-%d-%d" % (packer_type, i) - value = "value-s-%d-%d-%.0f" % (packer_type, i, start) + for i in range(NUM_ITERATIONS): + if is_unicode: + key = "key-%d-u-%d-%d-café" % (binary, packer_type, i) + value = "value-%d-u-%d-%d-café" % (binary, packer_type, i) + else: + key = "key-%d-s-%d-%d-cafe" % (binary, packer_type, i) + value = "value-%d-s-%d-%d-cafe" % (binary, packer_type, i) - kt.set(key, value) - output = kt.get(key) + kt.set(key, value) + output = kt.get(key) - if output != value: - bad += 1 + if output != value: + bad += 1 - kt.remove(key) - output = kt.get(key) + kt.remove(key) + output = kt.get(key) - if output is not None: - bad += 1 + if output is not None: + bad += 1 - duration = time() - start - rate = NUM_ITERATIONS / duration + duration = time() - start + rate = NUM_ITERATIONS / duration - print("%-15s | %-15s | %-16s | %5.2f s | %10.2f ips | %-6s" % - (binary, is_unicode, packer_name, duration, rate, - "No" if bad > 0 else "Yes")) + print("%-15s | %-16s | %-16s | %5.2f s | %10.2f ips | %-6s" % + (binary, is_unicode, packer_name, duration, rate, + "No" if bad > 0 else "Yes")) - kt.close() + kt.close() if __name__ == "__main__": From 13af102acedf2954b531cf576f532fb0f34c3834 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 18:08:09 +0000 Subject: [PATCH 21/62] Updated README with Python 3 support. --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 4f6baa6..c9d53c3 100644 --- a/README +++ b/README @@ -16,7 +16,7 @@ interface. This fork is backwards compatible, and includes cursor support and significant performance improvements over the original -library. +library. It also supports both Python 2.x and 3.x. The more efficient binary protocol is also supported along with the HTTP protocol. It provides a performance improvement of up From 88aecead05a8ec4479e92f1dfa24bb0a02756dba Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 18:15:30 +0000 Subject: [PATCH 22/62] Updated information for PyPI --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index b41af46..3a50761 100644 --- a/setup.py +++ b/setup.py @@ -10,15 +10,15 @@ setup( author='Toru Maesaka', author_email='dev@torum.net', - maintainer='Stephen Hamer', - maintainer_email='stephen.hamer@upverter.com', + maintainer='Carlos Rodrigues', + maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.1', + version='0.5.2', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], requires=['percentcoding', 'simplejson'], - url='https://github.com/upverter/python-kyototycoon', + url='https://github.com/carlosefr/python-kyototycoon', zip_safe=False ) From c80926822be185f1679728792297f405783c9822 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 21:13:42 +0000 Subject: [PATCH 23/62] Drop percentcoding requirement (no measurable performance improvement). All tests successful on both Python 2.7 and 3.2. --- kyototycoon/kt_http.py | 81 +++++++++++++++++++++++++----------------- setup.py | 2 +- tests/t_simple.py | 4 ++- tests/t_utils.py | 13 +++---- 4 files changed, 57 insertions(+), 43 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 6f0d957..ed7ad39 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -18,16 +18,16 @@ import http.client as httplib try: - from percentcoding import quote, unquote + from urllib import quote as _quote + from urllib import quote as _quote_from_bytes + from urllib import unquote as unquote_to_bytes except ImportError: - try: - from urllib import quote as _quote - from urllib import unquote - except ImportError: - from urllib.parse import quote as _quote - from urllib.parse import unquote + from urllib.parse import quote as _quote + from urllib.parse import quote_from_bytes as _quote_from_bytes + from urllib.parse import unquote_to_bytes - quote = lambda s: _quote(s, safe='') +quote = lambda s: _quote(s, safe='') +quote_from_bytes = lambda s: _quote_from_bytes(s, safe='') try: import cPickle as pickle @@ -51,14 +51,18 @@ KT_PACKER_STRING = 3 def _dict_to_tsv(dict): - return '\n'.join(quote(k) + '\t' + quote(str(v)) for (k, v) in dict.items()) + lines = [] + for k, v in dict.items(): + quoted = quote_from_bytes(v) if isinstance(v, bytes) else quote(str(v)) + lines.append(quote(k) + '\t' + quoted) + return '\n'.join(lines) def _content_type_decoder(content_type=''): ''' Select the appropriate decoding function to use based on the response headers. ''' if content_type.endswith('colenc=B'): return base64.decodestring elif content_type.endswith('colenc=U'): - return unquote + return unquote_to_bytes else: return lambda x: x @@ -66,8 +70,8 @@ def _tsv_to_dict(tsv_str, content_type=''): decode = _content_type_decoder(content_type) rv = {} - for row in tsv_str.split('\n'): - kv = row.split('\t') + for row in tsv_str.split(b'\n'): + kv = row.split(b'\t') if len(kv) == 2: rv[decode(kv[0])] = decode(kv[1]) return rv @@ -76,8 +80,8 @@ def _tsv_to_list(tsv_str, content_type=''): decode = _content_type_decoder(content_type) rv = [] - for row in tsv_str.split('\n'): - kv = row.split('\t') + for row in tsv_str.split(b'\n'): + kv = row.split(b'\t') if len(kv) == 2: pair = (decode(kv[0]), decode(kv[1])) rv.append(pair) @@ -95,7 +99,6 @@ def __init__(self, protocol_handler): self.err = kt_error.KyotoTycoonError() self.pack = self.protocol_handler.pack self.unpack = self.protocol_handler.unpack - self.pack_type = self.protocol_handler.pack_type def __enter__(self): return self @@ -338,6 +341,7 @@ def delete(self): class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.err = kt_error.KyotoTycoonError() + self.pack_type = pack_type if pack_type == KT_PACKER_PICKLE: self.pack = lambda data: pickle.dumps(data, pickle_protocol) @@ -454,7 +458,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): return False self.err.set_success() - return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))['num']) + return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def remove_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): @@ -485,7 +489,7 @@ def remove_bulk(self, keys, atomic, db): return False self.err.set_success() - return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))['num']) + return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def get_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): @@ -518,7 +522,7 @@ def get_bulk(self, keys, atomic, db): rv = {} res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - n = res_dict.pop('num') + n = res_dict.pop(b'num') if n == '0': self.err.set_error(self.err.NOTFOUND) @@ -526,7 +530,7 @@ def get_bulk(self, keys, atomic, db): for k, v in res_dict.items(): if v is not None: - rv[k[1:]] = self.unpack(v) + rv[k.decode('UTF-8')[1:]] = self.unpack(v) self.err.set_success() return rv @@ -590,8 +594,8 @@ def match_prefix(self, prefix, max, db): return False res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) - if len(res_list) == 0 or res_list[-1][0] != 'num': - self.err.set_err(self.err.EMISC) + if len(res_list) == 0 or res_list[-1][0] != b'num': + self.err.set_error(self.err.EMISC) return False num_key, num = res_list.pop() if num == '0': @@ -599,7 +603,7 @@ def match_prefix(self, prefix, max, db): return [] for k, v in res_list: - rv.append(k[1:]) + rv.append(k.decode('UTF-8')[1:]) self.err.set_success() return rv @@ -628,8 +632,8 @@ def match_regex(self, regex, max, db): rv = [] res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) - if len(res_list) == 0 or res_list[-1][0] != 'num': - self.err.set_err(self.err.EMISC) + if len(res_list) == 0 or res_list[-1][0] != b'num': + self.err.set_error(self.err.EMISC) return False num_key, num = res_list.pop() if num == '0': @@ -637,7 +641,7 @@ def match_regex(self, regex, max, db): return [] for k, v in res_list: - rv.append(k[1:]) + rv.append(k.decode('UTF-8')[1:]) self.err.set_success() return rv @@ -647,9 +651,10 @@ def set(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False - if db: - key = '/%s/%s' % (db, key) key = quote(key.encode('UTF-8')) + if db: + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + value = self.pack(value) self.err.set_success() @@ -736,10 +741,10 @@ def replace(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False + key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (db, key) + key = '/%s/%s' % (quote(db.encode('UTF-8')), key) - key = quote(key.encode('UTF-8')) value = self.pack(value) status = self._rest_put('replace', key, value, expire) @@ -792,7 +797,7 @@ def increment(self, key, delta, expire, db): return None self.err.set_success() - return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))['num']) + return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def increment_double(self, key, delta, expire, db): if key is None: @@ -814,7 +819,7 @@ def increment_double(self, key, delta, expire, db): return None self.err.set_success() - return float(_tsv_to_dict(body, res.getheader('Content-Type', ''))['num']) + return float(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def report(self): self.conn.request('GET', '/rpc/report') @@ -823,8 +828,13 @@ def report(self): self.err.set_error(self.err.EMISC) return None + res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) + status_dict = {} + for k, v in res_dict.items(): + status_dict[k.decode('UTF-8')] = v.decode('UTF-8') + self.err.set_success() - return _tsv_to_dict(body, res.getheader('Content-Type', '')) + return status_dict def status(self, db=None): url = '/rpc/status' @@ -839,8 +849,13 @@ def status(self, db=None): self.err.set_error(self.err.EMISC) return None + res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) + status_dict = {} + for k, v in res_dict.items(): + status_dict[k.decode('UTF-8')] = v.decode('UTF-8') + self.err.set_success() - return _tsv_to_dict(body, res.getheader('Content-Type', '')) + return status_dict def clear(self, db=None): url = '/rpc/clear' diff --git a/setup.py b/setup.py index 3a50761..c750feb 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], - requires=['percentcoding', 'simplejson'], + requires=['simplejson'], url='https://github.com/carlosefr/python-kyototycoon', zip_safe=False ) diff --git a/tests/t_simple.py b/tests/t_simple.py index 7f75efe..3de7b82 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -221,6 +221,8 @@ def test_report(self): report = self.kt_handle.report() assert report is not None + self.assertTrue(int(report['serv_conn_count']) > 0) + def test_status(self): self.assertTrue(self.kt_handle.clear()) status = None @@ -256,7 +258,7 @@ def test_match_prefix(self): list = self.kt_handle.match_prefix('abcd') self.assertEqual(len(list), 5) list = self.kt_handle.match_prefix('abc', 1) - self.assertEqual(list[0], 'abc') + self.assertEqual(list[0][:3], 'abc') def test_match_regex(self): self.assertTrue(self.kt_handle.clear()) diff --git a/tests/t_utils.py b/tests/t_utils.py index 7c1bbd0..fd5d203 100644 --- a/tests/t_utils.py +++ b/tests/t_utils.py @@ -15,18 +15,15 @@ def setUp(self): def test_packer(self): stri = 'hello world sir' - buf = self.kt_core._pickle_packer(stri) - if type(stri) is str: - assert buf == stri - else: - assert buf != stri - ret = self.kt_core._pickle_unpacker(buf) + buf = self.kt_core.pack(stri) + assert buf != stri + ret = self.kt_core.unpack(buf) self.assertEqual(stri, ret) num = 777 - buf = self.kt_core._pickle_packer(num) + buf = self.kt_core.pack(num) assert buf != num - ret = self.kt_core._pickle_unpacker(buf) + ret = self.kt_core.unpack(buf) self.assertEqual(type(num), type(ret)) self.assertEqual(num, ret) From 73568a9ea43688cb458024b88dc70961259c59c1 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 21:43:35 +0000 Subject: [PATCH 24/62] Make cursors work for Python 3.x. Add test suite for cursors. --- kyototycoon/kt_http.py | 33 +++++++++++++++++---------------- tests/t_simple.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index ed7ad39..b55f31f 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -110,8 +110,7 @@ def __exit__(self, type, value, tb): def jump(self, key=None, db=None): path = '/rpc/cur_jump' if db: - db = quote(db) - path += '?DB=' + db + path = '%s?DB=%s' % (path, quote(db.encode('UTF-8'))) request_dict = {} request_dict['CUR'] = self.cursor_id @@ -133,8 +132,7 @@ def jump(self, key=None, db=None): def jump_back(self, key=None, db=None): path = '/rpc/cur_jump_back' if db: - db = quote(db) - path += '?DB=' + db + path = '%s?DB=%s' % (path, quote(db.encode('UTF-8'))) request_dict = {} request_dict['CUR'] = self.cursor_id @@ -248,7 +246,7 @@ def get_key(self, step=False): return False self.err.set_success() - return _tsv_to_dict(body, res.getheader('Content-Type', ''))['key'] + return _tsv_to_dict(body, res.getheader('Content-Type', ''))[b'key'].decode('UTF-8') def get_value(self, step=False): path = '/rpc/cur_get_value' @@ -268,7 +266,7 @@ def get_value(self, step=False): return False self.err.set_success() - return self.unpack(_tsv_to_dict(body, res.getheader('Content-Type', ''))['value']) + return self.unpack(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'value']) def get(self, step=False): path = '/rpc/cur_get' @@ -291,10 +289,11 @@ def get(self, step=False): self.err.set_error(self.err.EMISC) return False, False - self.err.set_success() res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - key = res_dict['key'] - value = self.unpack(res_dict['value']) + key = res_dict[b'key'].decode('UTF-8') + value = self.unpack(res_dict[b'value']) + + self.err.set_success() return key, value def seize(self): @@ -312,11 +311,13 @@ def seize(self): self.err.set_error(self.err.EMISC) return False - self.err.set_success() res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - res_dict['key'] = res_dict['key'] - res_dict['value'] = self.unpack(res_dict['value']) - return res_dict + seize_dict = {} + seize_dict['key'] = res_dict[b'key'].decode('UTF-8') + seize_dict['value'] = self.unpack(res_dict[b'value']) + + self.err.set_success() + return seize_dict def delete(self): path = '/rpc/cur_delete' @@ -829,12 +830,12 @@ def report(self): return None res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - status_dict = {} + report_dict = {} for k, v in res_dict.items(): - status_dict[k.decode('UTF-8')] = v.decode('UTF-8') + report_dict[k.decode('UTF-8')] = v.decode('UTF-8') self.err.set_success() - return status_dict + return report_dict def status(self, db=None): url = '/rpc/status' diff --git a/tests/t_simple.py b/tests/t_simple.py index 3de7b82..fa688b2 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -273,5 +273,36 @@ def test_match_regex(self): list = self.kt_handle.match_regex('e$') self.assertEqual(len(list), 1) + def test_cursor(self): + self.assertTrue(self.kt_handle.clear()) + self.assertTrue(self.kt_handle.set('abc', 'val')) + self.assertTrue(self.kt_handle.set('abcd', 'val')) + self.assertTrue(self.kt_handle.set('abcde', 'val')) + + cur = self.kt_handle.cursor() + self.assertTrue(cur.jump()) + while cur.step(): + self.assertEqual(cur.get_key()[:3], 'abc') + self.assertEqual(cur.get_value(), 'val') + + pair = cur.get() + self.assertEqual(len(pair), 2) + self.assertEqual(pair[0][:3], 'abc') + self.assertEqual(pair[1], 'val') + + dict = cur.seize() + self.assertEqual(len(dict), 2) + self.assertEqual(dict['key'][:3], 'abc') + self.assertEqual(dict['value'], 'val') + + self.assertTrue(cur.remove()) + self.assertFalse(cur.get_key()) + self.assertFalse(cur.get_value()) + self.assertEqual(cur.get(), (False, False)) + self.assertEqual(cur.seize(), False) + + self.assertTrue(cur.delete()) + + if __name__ == '__main__': unittest.main() From 707e0320f038fba04325253bbd18767828065239 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 21:46:51 +0000 Subject: [PATCH 25/62] Ignore Python 3's cache directory. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 57e683c..5e31945 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc /build +__pycache__ From d5c71b89d4c6711b692ff620faacc2439f2eae12 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 22:22:22 +0000 Subject: [PATCH 26/62] Test suite for the binary protocol. Fixed inconsistencies with error codes. --- kyototycoon/kt_binary.py | 28 ++++++- kyototycoon/kt_http.py | 9 ++- tests/t_simple.py | 3 +- tests/t_simple_binary.py | 153 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 tests/t_simple_binary.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index efd8fda..4fcad9e 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -99,6 +99,10 @@ def set_bulk(self, kv_dict, expire, atomic, db): request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] for key, value in kv_dict.items(): + if key is None: + self.err.set_error(self.err.LOGIC) + return 0 + key = key.encode('UTF-8') value = self.pack(value) @@ -114,8 +118,11 @@ def set_bulk(self, kv_dict, expire, atomic, db): return False num_items, = struct.unpack('!I', self._read(4)) + if num_items > 0: + self.err.set_success() + else: + self.err.set_error(self.err.NOTFOUND) - self.err.set_success() return num_items def remove_bulk(self, keys, atomic, db): @@ -129,6 +136,10 @@ def remove_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_REMOVE_BULK, 0, len(keys))] for key in keys: + if key is None: + self.err.set_error(self.err.LOGIC) + return 0 + key = key.encode('UTF-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -141,8 +152,11 @@ def remove_bulk(self, keys, atomic, db): return False num_items, = struct.unpack('!I', self._read(4)) + if num_items > 0: + self.err.set_success() + else: + self.err.set_error(self.err.NOTFOUND) - self.err.set_success() return num_items def get_bulk(self, keys, atomic, db): @@ -156,6 +170,10 @@ def get_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_GET_BULK, 0, len(keys))] for key in keys: + if key is None: + self.err.set_error(self.err.LOGIC) + return 0 + key = key.encode('UTF-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -174,6 +192,12 @@ def get_bulk(self, keys, atomic, db): key = self._read(key_length) value = self._read(value_length) items[key.decode('UTF-8')] = self.unpack(value) + + if num_items > 0: + self.err.set_success() + else: + self.err.set_error(self.err.NOTFOUND) + return items def get_int(self, key, db=None): diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index b55f31f..e423864 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -489,8 +489,13 @@ def remove_bulk(self, keys, atomic, db): self.err.set_error(self.err.EMISC) return False - self.err.set_success() - return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) + count = int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) + if count > 0: + self.err.set_success() + else: + self.err.set_error(self.err.NOTFOUND) + + return count def get_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): diff --git a/tests/t_simple.py b/tests/t_simple.py index fa688b2..f5e8780 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -8,10 +8,11 @@ import config import unittest from kyototycoon import KyotoTycoon +from kyototycoon.kt_http import KT_PACKER_PICKLE class UnitTest(unittest.TestCase): def setUp(self): - self.kt_handle = KyotoTycoon() + self.kt_handle = KyotoTycoon(binary=False, pack_type=KT_PACKER_PICKLE) self.kt_handle.open() self.LARGE_KEY_LEN = 8000 diff --git a/tests/t_simple_binary.py b/tests/t_simple_binary.py new file mode 100644 index 0000000..0a35568 --- /dev/null +++ b/tests/t_simple_binary.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. + +import config +import unittest +from kyototycoon import KyotoTycoon +from kyototycoon.kt_binary import KT_PACKER_PICKLE + +class UnitTest(unittest.TestCase): + def setUp(self): + # For operations not supported by the binary protocol, but useful for testing it... + self.kt_handle_http = KyotoTycoon(binary=False) + self.kt_handle_http.open() + + self.kt_handle = KyotoTycoon(binary=True, pack_type=KT_PACKER_PICKLE) + self.kt_handle.open() + self.LARGE_KEY_LEN = 8000 + + def test_set(self): + self.assertTrue(self.kt_handle_http.clear()) + error = self.kt_handle.error() + + self.assertTrue(self.kt_handle.set('key', 'value')) + self.assertEqual(error.code(), error.SUCCESS) + + self.assertTrue(self.kt_handle.set('k e y', 'v a l u e')) + self.assertTrue(self.kt_handle.set('k\te\ty', 'tabbed')) + + self.assertEqual(self.kt_handle.get('key'), 'value') + self.assertEqual(self.kt_handle.get('k e y'), 'v a l u e') + self.assertEqual(self.kt_handle.get('k\te\ty'), 'tabbed') + + self.assertTrue(self.kt_handle.set('\\key', '\\xxx')) + self.assertEqual(self.kt_handle.get('\\key'), '\\xxx') + self.assertEqual(self.kt_handle_http.count(), 4) + + self.assertTrue(self.kt_handle.set('tabbed\tkey', 'tabbled\tvalue')) + self.assertTrue(self.kt_handle.get('tabbed\tkey')) + + self.assertTrue(self.kt_handle.set('url1', 'http://github.com')) + self.assertTrue(self.kt_handle.set('url2', 'https://github.com/')) + self.assertTrue(self.kt_handle.set('url3', 'https://github.com/blog/')) + + self.assertTrue(self.kt_handle.set('http://github.com', 'url1')) + self.assertTrue(self.kt_handle.set('https://github.com', 'url2')) + self.assertTrue(self.kt_handle.set('https://github.com/blog/', 'url3')) + + self.assertFalse(self.kt_handle.set(None, 'value')) + self.assertEqual(error.code(), error.LOGIC) + + self.assertFalse(self.kt_handle.get(None)) + self.assertEqual(error.code(), error.LOGIC) + + self.assertEqual(self.kt_handle.get('non_existent'), None) + self.assertEqual(error.code(), error.NOTFOUND) + + self.assertTrue(self.kt_handle.set('cb', 1791)) + self.assertEqual(error.code(), error.SUCCESS) + self.assertEqual(self.kt_handle.get('cb'), 1791) + self.assertEqual(error.code(), error.SUCCESS) + + self.assertTrue(self.kt_handle.set('cb', 1791.1226)) + self.assertEqual(self.kt_handle.get('cb'), 1791.1226) + + def test_remove(self): + self.assertTrue(self.kt_handle_http.clear()) + error = self.kt_handle.error() + + self.assertFalse(self.kt_handle.remove('must fail key')) + self.assertEqual(error.code(), error.NOTFOUND) + self.assertTrue(self.kt_handle.set('deleteable key', 'xxx')) + self.assertEqual(error.code(), error.SUCCESS) + self.assertTrue(self.kt_handle.remove('deleteable key')) + self.assertEqual(error.code(), error.SUCCESS) + self.assertFalse(self.kt_handle.remove(None)) + self.assertEqual(error.code(), error.LOGIC) + + def test_set_bulk(self): + self.assertTrue(self.kt_handle_http.clear()) + + dict = { + 'k1': 'one', + 'k2': 'two', + 'k3': 'three', + 'k4': 'four', + 'k\n5': 'five', + 'k\t6': 'six', + 'k7': 111 + } + + n = self.kt_handle.set_bulk(dict) + self.assertEqual(len(dict), n) + self.assertEqual(self.kt_handle.get('k1'), 'one') + self.assertEqual(self.kt_handle.get('k2'), 'two') + self.assertEqual(self.kt_handle.get('k3'), 'three') + self.assertEqual(self.kt_handle.get('k4'), 'four') + self.assertEqual(self.kt_handle.get('k\n5'), 'five') + self.assertEqual(self.kt_handle.get('k\t6'), 'six') + self.assertEqual(self.kt_handle.get('k7'), 111) + + d = self.kt_handle.get_bulk(['k1', 'k2', 'k3', 'k4', + 'k\n5', 'k\t6', 'k7']) + + self.assertEqual(len(d), len(dict)) + self.assertEqual(d, dict) + + self.assertEqual(self.kt_handle_http.count(), 7) + n = self.kt_handle.remove_bulk(['k1', 'k2', 'k\t6']) + self.assertEqual(self.kt_handle_http.count(), 4) + n = self.kt_handle.remove_bulk(['k3'], atomic=True) + self.assertEqual(self.kt_handle_http.count(), 3) + + def test_get_bulk(self): + self.assertTrue(self.kt_handle_http.clear()) + self.assertTrue(self.kt_handle.set('a', 'one')) + self.assertTrue(self.kt_handle.set('b', 'two')) + self.assertTrue(self.kt_handle.set('c', 'three')) + self.assertTrue(self.kt_handle.set('d', 'four')) + + d = self.kt_handle.get_bulk(['a','b','c','d']) + assert d is not None + + self.assertEqual(d['a'], 'one') + self.assertEqual(d['b'], 'two') + self.assertEqual(d['c'], 'three') + self.assertEqual(d['d'], 'four') + self.assertEqual(len(d), 4) + + d = self.kt_handle.get_bulk(['a','x','y','d']) + self.assertEqual(len(d), 2) + d = self.kt_handle.get_bulk(['w','x','y','z']) + self.assertEqual(len(d), 0) + d = self.kt_handle.get_bulk([]) + self.assertEqual(d, {}) + + def test_large_key(self): + large_key = 'x' * self.LARGE_KEY_LEN + self.assertTrue(self.kt_handle.set(large_key, 'value')) + self.assertEqual(self.kt_handle.get(large_key), 'value') + + def test_error(self): + self.assertTrue(self.kt_handle_http.clear()) + kt_error = self.kt_handle.error() + assert kt_error is not None + self.assertEqual(kt_error.code(), kt_error.SUCCESS) + + +if __name__ == '__main__': + unittest.main() From 638f1ffe66ac7300bb739e845a19fff114c5e252 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 22:29:11 +0000 Subject: [PATCH 27/62] Added Makefile to clean the tree. --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f59b9fe --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +clean: + find . -name '*~' | xargs rm -f + find . -name '*.pyc' | xargs rm -f + rm -rf build/ + rm -rf tests/__pycache__ + rm -rf kyototycoon/__pycache__ From 2ee63ecd46b0d87d62895220e56e577cabec7923 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 18 Jan 2014 23:27:17 +0000 Subject: [PATCH 28/62] Fixed cursor tests. --- tests/t_simple.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/t_simple.py b/tests/t_simple.py index f5e8780..75f54c6 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -276,13 +276,15 @@ def test_match_regex(self): def test_cursor(self): self.assertTrue(self.kt_handle.clear()) + self.assertEqual(self.kt_handle.count(), 0) self.assertTrue(self.kt_handle.set('abc', 'val')) self.assertTrue(self.kt_handle.set('abcd', 'val')) self.assertTrue(self.kt_handle.set('abcde', 'val')) + self.assertEqual(self.kt_handle.count(), 3) cur = self.kt_handle.cursor() self.assertTrue(cur.jump()) - while cur.step(): + while True: self.assertEqual(cur.get_key()[:3], 'abc') self.assertEqual(cur.get_value(), 'val') @@ -291,18 +293,40 @@ def test_cursor(self): self.assertEqual(pair[0][:3], 'abc') self.assertEqual(pair[1], 'val') - dict = cur.seize() - self.assertEqual(len(dict), 2) - self.assertEqual(dict['key'][:3], 'abc') - self.assertEqual(dict['value'], 'val') + self.assertTrue(cur.set_value('foo')) + self.assertEqual(cur.get_value(), 'foo') + + if pair[0] == 'abcd': + self.assertTrue(cur.remove()) + break - self.assertTrue(cur.remove()) - self.assertFalse(cur.get_key()) - self.assertFalse(cur.get_value()) - self.assertEqual(cur.get(), (False, False)) - self.assertEqual(cur.seize(), False) + if not cur.step(): + break self.assertTrue(cur.delete()) + self.assertEqual(self.kt_handle.count(), 2) + + self.assertTrue(self.kt_handle.set('zabc', 'val')) + self.assertTrue(self.kt_handle.set('zabcd', 'val')) + self.assertTrue(self.kt_handle.set('zabcde', 'val')) + self.assertEqual(self.kt_handle.count(), 5) + + cur = self.kt_handle.cursor() + self.assertTrue(cur.jump_back()) + while True: + pair = cur.get() + + if pair[0] == 'zabc': + dict = cur.seize() + self.assertEqual(len(dict), 2) + self.assertEqual(dict['key'][:4], 'zabc') + self.assertEqual(dict['value'], 'val') + + if not cur.step_back(): + break + + self.assertTrue(cur.delete()) + self.assertEqual(self.kt_handle.count(), 4) if __name__ == '__main__': From 99610621a15833e96a6df8b9b1e5a6122baf073c Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 19 Jan 2014 16:03:15 +0000 Subject: [PATCH 29/62] Handle integer database IDs correctly over HTTP. --- kyototycoon/kt_http.py | 28 ++++++++++++++++++---------- tests/t_simple.py | 4 ++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index e423864..55191d9 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -110,12 +110,13 @@ def __exit__(self, type, value, tb): def jump(self, key=None, db=None): path = '/rpc/cur_jump' if db: - path = '%s?DB=%s' % (path, quote(db.encode('UTF-8'))) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + path = '%s?DB=%s' % (path, db) request_dict = {} request_dict['CUR'] = self.cursor_id if key: - request_dict['key'] = key + request_dict['key'] = key.encode('UTF-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, @@ -132,12 +133,13 @@ def jump(self, key=None, db=None): def jump_back(self, key=None, db=None): path = '/rpc/cur_jump_back' if db: - path = '%s?DB=%s' % (path, quote(db.encode('UTF-8'))) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + path = '%s?DB=%s' % (path, db) request_dict = {} request_dict['CUR'] = self.cursor_id if key: - request_dict['key'] = key + request_dict['key'] = key.encode('UTF-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, @@ -412,7 +414,8 @@ def get(self, key, db=None): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) self.conn.request('GET', key) res, body = self.getresponse() @@ -548,7 +551,8 @@ def get_int(self, key, db=None): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) self.conn.request('GET', key) @@ -659,7 +663,8 @@ def set(self, key, value, expire, db): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) value = self.pack(value) @@ -680,7 +685,8 @@ def add(self, key, value, expire, db): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) value = self.pack(value) status = self._rest_put('add', key, value, expire) @@ -730,7 +736,8 @@ def remove(self, key, db): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) self.conn.request('DELETE', key) @@ -749,7 +756,8 @@ def replace(self, key, value, expire, db): key = quote(key.encode('UTF-8')) if db: - key = '/%s/%s' % (quote(db.encode('UTF-8')), key) + db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + key = '/%s/%s' % (db, key) value = self.pack(value) status = self._rest_put('replace', key, value, expire) diff --git a/tests/t_simple.py b/tests/t_simple.py index 75f54c6..25a67c5 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -312,7 +312,7 @@ def test_cursor(self): self.assertEqual(self.kt_handle.count(), 5) cur = self.kt_handle.cursor() - self.assertTrue(cur.jump_back()) + self.assertTrue(cur.jump(key='zabc')) while True: pair = cur.get() @@ -322,7 +322,7 @@ def test_cursor(self): self.assertEqual(dict['key'][:4], 'zabc') self.assertEqual(dict['value'], 'val') - if not cur.step_back(): + if not cur.step(): break self.assertTrue(cur.delete()) From 36c7752400fb373df5b2c6be39da99ee7aa05234 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 1 Feb 2014 17:37:47 +0000 Subject: [PATCH 30/62] Docstrings for public methods. Fix append behavior (support all packer types for string values and keep the original type - str/bytes/unicode - intact). --- kyototycoon/__init__.py | 8 +-- kyototycoon/kt_http.py | 124 ++++++++++++++++++++++--------------- kyototycoon/kyototycoon.py | 63 +++++++++++++++++-- tests/t_simple.py | 8 +++ 4 files changed, 140 insertions(+), 63 deletions(-) diff --git a/kyototycoon/__init__.py b/kyototycoon/__init__.py index 5731f2c..577edfa 100644 --- a/kyototycoon/__init__.py +++ b/kyototycoon/__init__.py @@ -1,11 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -try: - # For Python 3.x... - from kyototycoon.kyototycoon import KyotoTycoon -except ImportError: - # For Python 2.x... - from kyototycoon import * +from .kyototycoon import KyotoTycoon +from .kyototycoon import KT_DEFAULT_HOST, KT_DEFAULT_PORT, KT_DEFAULT_TIMEOUT # vim: set expandtab ts=4 sw=4 diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 55191d9..763a595 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -9,6 +9,7 @@ import base64 import struct import time +import sys from . import kt_error @@ -42,7 +43,7 @@ # Stick with URL encoding for now. Eventually run a benchmark # to evaluate what the most approariate encoding algorithm is. KT_HTTP_HEADER = { - 'Content-Type' : 'text/tab-separated-values; colenc=U', + 'Content-Type' : 'text/tab-separated-values; colenc=U', } KT_PACKER_CUSTOM = 0 @@ -108,6 +109,8 @@ def __exit__(self, type, value, tb): self.delete() def jump(self, key=None, db=None): + '''Jump the cursor to a record (first record if "None") for forward scan.''' + path = '/rpc/cur_jump' if db: db = db if isinstance(db, int) else quote(db.encode('UTF-8')) @@ -119,8 +122,7 @@ def jump(self, key=None, db=None): request_dict['key'] = key.encode('UTF-8') request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -131,6 +133,8 @@ def jump(self, key=None, db=None): return True def jump_back(self, key=None, db=None): + '''Jump the cursor to a record (last record if "None") for forward scan.''' + path = '/rpc/cur_jump_back' if db: db = db if isinstance(db, int) else quote(db.encode('UTF-8')) @@ -142,8 +146,7 @@ def jump_back(self, key=None, db=None): request_dict['key'] = key.encode('UTF-8') request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -154,14 +157,15 @@ def jump_back(self, key=None, db=None): return True def step(self): + '''Step the cursor to the next record.''' + path = '/rpc/cur_step' request_dict = {} request_dict['CUR'] = self.cursor_id request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -172,14 +176,15 @@ def step(self): return True def step_back(self): + '''Step the cursor to the previous record.''' + path = '/rpc/cur_step_back' request_dict = {} request_dict['CUR'] = self.cursor_id request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -190,6 +195,8 @@ def step_back(self): return True def set_value(self, value, step=False, xt=None): + '''Set the value for the current record.''' + path = '/rpc/cur_set_value' request_dict = {} @@ -201,8 +208,7 @@ def set_value(self, value, step=False, xt=None): request_dict['xt'] = xt request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -213,14 +219,15 @@ def set_value(self, value, step=False, xt=None): return True def remove(self): + '''Remove the current record.''' + path = '/rpc/cur_remove' request_dict = {} request_dict['CUR'] = self.cursor_id request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -231,6 +238,8 @@ def remove(self): return True def get_key(self, step=False): + '''Get the key for the current record.''' + path = '/rpc/cur_get_key' request_dict = {} @@ -239,8 +248,7 @@ def get_key(self, step=False): request_dict['step'] = True request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -251,6 +259,8 @@ def get_key(self, step=False): return _tsv_to_dict(body, res.getheader('Content-Type', ''))[b'key'].decode('UTF-8') def get_value(self, step=False): + '''Get the value for the current record.''' + path = '/rpc/cur_get_value' request_dict = {} @@ -259,8 +269,7 @@ def get_value(self, step=False): request_dict['step'] = True request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -271,6 +280,8 @@ def get_value(self, step=False): return self.unpack(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'value']) def get(self, step=False): + '''Get a (key,value) pair for the current record.''' + path = '/rpc/cur_get' request_dict = {} @@ -279,8 +290,7 @@ def get(self, step=False): request_dict['step'] = True request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status == 404: @@ -299,14 +309,15 @@ def get(self, step=False): return key, value def seize(self): + '''Get a (key,value) pair for the current record, and remove it atomically.''' + path = '/rpc/cur_seize' request_dict = {} request_dict['CUR'] = self.cursor_id request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -322,14 +333,15 @@ def seize(self): return seize_dict def delete(self): + '''Delete the cursor.''' + path = '/rpc/cur_delete' request_dict = {} request_dict['CUR'] = self.cursor_id request_body = _dict_to_tsv(request_dict) - self.protocol_handler.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() if res.status != 200: @@ -368,8 +380,8 @@ def cursor(self): return Cursor(self) def open(self, host, port, timeout): - # Save connection parameters so the connection can be re-established - # on "Connection: close" response. + # Save connection parameters so the connection can be + # re-established on a "Connection: close" response... self.host = host self.port = port self.timeout = timeout @@ -453,8 +465,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): v = quote(self.pack(v)) request_body += '_' + k + '\t' + v + '\n' - self.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -484,8 +495,7 @@ def remove_bulk(self, keys, atomic, db): if db: db = quote(db) path += '?DB=' + db - self.conn.request('POST', path, body=request_header + request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -521,8 +531,7 @@ def get_bulk(self, keys, atomic, db): if db: db = quote(db) path += '?DB=' + db - self.conn.request('POST', path, body=request_header + request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -595,8 +604,7 @@ def match_prefix(self, prefix, max, db): request_dict['DB'] = db request_body = _dict_to_tsv(request_dict) - self.conn.request('POST', '/rpc/match_prefix', - body=request_body, headers=KT_HTTP_HEADER) + self.conn.request('POST', '/rpc/match_prefix', body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -632,8 +640,7 @@ def match_regex(self, regex, max, db): request_dict['max'] = max request_body = _dict_to_tsv(request_dict) - self.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -718,8 +725,7 @@ def cas(self, key, old_val, new_val, expire, db): request_body = _dict_to_tsv(request_dict) - self.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -773,20 +779,38 @@ def append(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) if key is None: return False - elif not isinstance(value, str): + + # Simultaneous support for Python 2/3 makes this cumbersome... + if sys.version_info[0] >= 3: + bytes_type = bytes + unicode_type = str + else: + bytes_type = str + unicode_type = unicode + + if (not isinstance(value, bytes_type) and + not isinstance(value, unicode_type)): return False - # Only handle Pickle for now. - if self.pack_type == KT_PACKER_PICKLE: - data = self.get(key) - if data is None: - data = value + data = self.get(key) + if data is None: + data = type(value)() + + if (not isinstance(data, bytes_type) and + not isinstance(data, unicode_type)): + return False + + if type(data) != type(value): + if isinstance(data, bytes_type): + value = value.encode('UTF-8') else: - data = data + value + value = value.decode('UTF-8') - if self.set(key, data, expire, db) is True: - self.err.set_success() - return True + data += value + + if self.set(key, data, expire, db) is True: + self.err.set_success() + return True self.err.set_error(self.err.EMISC) return False @@ -802,8 +826,7 @@ def increment(self, key, delta, expire, db): delta = int(delta) request_body = 'key\t%s\nnum\t%d\n' % (key, delta) - self.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -824,8 +847,7 @@ def increment_double(self, key, delta, expire, db): delta = float(delta) request_body = 'key\t%s\nnum\t%f\n' % (key, delta) - self.conn.request('POST', path, body=request_body, - headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index a8c827e..991440d 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -17,83 +17,134 @@ class KyotoTycoon(object): def __init__(self, binary=False, *args, **kwargs): - if binary: - self.core = kt_binary.ProtocolHandler(*args, **kwargs) - else: - self.core = kt_http.ProtocolHandler(*args, **kwargs) + '''Initialize a "Binary Protocol" or "HTTP" KyotoTycoon object.''' + + protocol = kt_binary if binary else kt_http + self.core = protocol.ProtocolHandler(*args, **kwargs) def error(self): + '''Return the error state from the last operation.''' + return self.core.error() - def open(self, host=KT_DEFAULT_HOST, port=KT_DEFAULT_PORT, - timeout=KT_DEFAULT_TIMEOUT): + def open(self, host=KT_DEFAULT_HOST, port=KT_DEFAULT_PORT, timeout=KT_DEFAULT_TIMEOUT): + '''Open a new connection to a KT server.''' + return self.core.open(host, port, timeout) def close(self): + '''Close an open connection to the KT server.''' + return self.core.close() def report(self): + '''Get a server information report.''' + return self.core.report() def status(self, db=None): + '''Get status information for the database.''' + return self.core.status(db) def clear(self, db=None): + '''Remove all records in the database.''' + return self.core.clear(db) def count(self, db=None): + '''Number of records in the database.''' + return self.core.count(db) def size(self, db=None): + '''Current database size (in bytes).''' + return self.core.size(db) def set(self, key, value, expire=None, db=None): + '''Set the value for a record.''' + return self.core.set(key, value, expire, db) def add(self, key, value, expire=None, db=None): + '''Set the value for a record (does nothing if the record already exists).''' + return self.core.add(key, value, expire, db) def replace(self, key, value, expire=None, db=None): + '''Replace the value of an existing record.''' + return self.core.replace(key, value, expire, db) def append(self, key, value, expire=None, db=None): + '''Append "value" to the string value of a record.''' + return self.core.append(key, value, expire, db) def increment(self, key, delta, expire=None, db=None): + '''Add "delta" to the numeric integer value of a record.''' + return self.core.increment(key, delta, expire, db) def increment_double(self, key, delta, expire=None, db=None): + '''Add "delta" to the numeric double value of a record.''' + return self.core.increment_double(key, delta, expire, db) def cas(self, key, old_val=None, new_val=None, expire=None, db=None): + '''If the old value of a record is "old_val", replace it with "new_val".''' + return self.core.cas(key, old_val, new_val, expire, db) def remove(self, key, db=None): + '''Remove a record.''' + return self.core.remove(key, db) def get(self, key, db=None): + '''Retrieve the value for a record.''' + return self.core.get(key, db) def get_int(self, key, db=None): + '''Retrieve the numeric integer value for a record.''' + return self.core.get_int(key, db) def set_bulk(self, kv_dict, expire=None, atomic=True, db=None): + '''Set the values for several records at once.''' + return self.core.set_bulk(kv_dict, expire, atomic, db) def remove_bulk(self, keys, atomic=True, db=None): + '''Remove several records at once.''' + return self.core.remove_bulk(keys, atomic, db) def get_bulk(self, keys, atomic=True, db=None): + '''Retrieve the values for several records at once.''' + return self.core.get_bulk(keys, atomic, db) def vacuum(self, db=None): + '''Scan the database and eliminate regions of expired records.''' + return self.core.vacuum(db) def match_prefix(self, prefix, max=None, db=None): + '''Get keys matching a prefix string.''' + return self.core.match_prefix(prefix, max, db) def match_regex(self, regex, max=None, db=None): + '''Get keys matching a ragular expression string.''' + return self.core.match_regex(regex, max, db) def cursor(self): + '''Obtain a new (uninitialized) record cursor.''' + return self.core.cursor() + +# vim: set expandtab ts=4 sw=4 diff --git a/tests/t_simple.py b/tests/t_simple.py index 25a67c5..2b3b53b 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -137,6 +137,14 @@ def test_append(self): self.assertTrue(self.kt_handle.set('k3', 777)) self.assertFalse(self.kt_handle.append('k3', 111)) + self.assertTrue(self.kt_handle.set('k4', b'abc')) + self.assertTrue(self.kt_handle.append('k4', 'abc')) + self.assertEqual(self.kt_handle.get('k4'), b'abcabc') + + self.assertTrue(self.kt_handle.set('k5', 'abc')) + self.assertTrue(self.kt_handle.append('k5', b'abc')) + self.assertEqual(self.kt_handle.get('k5'), 'abcabc') + def test_add(self): self.assertTrue(self.kt_handle.clear()) self.assertTrue(self.kt_handle.set('stewie', 'griffin')) From eaa92c677e4ec19bd601520d3aab0b14a3efb586 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 1 Feb 2014 18:01:53 +0000 Subject: [PATCH 31/62] Better message for not-implemented methods. Use compact JSON on set. --- kyototycoon/kt_binary.py | 48 ++++++++++++++++---------------- kyototycoon/kt_http.py | 60 ++++++++++++++++++++-------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 4fcad9e..1a2d438 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -47,12 +47,12 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.unpack = lambda data: pickle.loads(data) elif pack_type == KT_PACKER_JSON: - self.pack = lambda data: json.dumps(data).encode('UTF-8') - self.unpack = lambda data: json.loads(data.decode('UTF-8')) + self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') + self.unpack = lambda data: json.loads(data.decode('utf-8')) elif pack_type == KT_PACKER_STRING: - self.pack = lambda data: data.encode('UTF-8') - self.unpack = lambda data: data.decode('UTF-8') + self.pack = lambda data: data.encode('utf-8') + self.unpack = lambda data: data.decode('utf-8') else: raise Exception('unsupported pack type specified') @@ -61,7 +61,7 @@ def error(self): return self.err def cursor(self): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def open(self, host, port, timeout): self.socket = socket.create_connection((host, port), timeout) @@ -103,7 +103,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): self.err.set_error(self.err.LOGIC) return 0 - key = key.encode('UTF-8') + key = key.encode('utf-8') value = self.pack(value) request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) @@ -140,7 +140,7 @@ def remove_bulk(self, keys, atomic, db): self.err.set_error(self.err.LOGIC) return 0 - key = key.encode('UTF-8') + key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -174,7 +174,7 @@ def get_bulk(self, keys, atomic, db): self.err.set_error(self.err.LOGIC) return 0 - key = key.encode('UTF-8') + key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -191,7 +191,7 @@ def get_bulk(self, keys, atomic, db): key_db, key_length, value_length, key_expire = struct.unpack('!HIIq', self._read(18)) key = self._read(key_length) value = self._read(value_length) - items[key.decode('UTF-8')] = self.unpack(value) + items[key.decode('utf-8')] = self.unpack(value) if num_items > 0: self.err.set_success() @@ -201,26 +201,26 @@ def get_bulk(self, keys, atomic, db): return items def get_int(self, key, db=None): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def vacuum(self, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def match_prefix(self, prefix, max, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def match_regex(self, regex, max, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def set(self, key, value, expire, db): numitems = self.set_bulk({key: value}, expire, True, db) return numitems > 0 def add(self, key, value, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def cas(self, key, old_val, new_val, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def remove(self, key, db): if key is None: @@ -231,31 +231,31 @@ def remove(self, key, db): return numitems > 0 def replace(self, key, value, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def append(self, key, value, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def increment(self, key, delta, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def increment_double(self, key, delta, expire, db): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def report(self): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def status(self, db=None): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def clear(self, db=None): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def count(self, db=None): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def size(self, db=None): - raise NotImplementedError + raise NotImplementedError('supported under the HTTP procotol only') def _write(self, data): self.socket.sendall(data) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 763a595..efd4ed8 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -113,13 +113,13 @@ def jump(self, key=None, db=None): path = '/rpc/cur_jump' if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path = '%s?DB=%s' % (path, db) request_dict = {} request_dict['CUR'] = self.cursor_id if key: - request_dict['key'] = key.encode('UTF-8') + request_dict['key'] = key.encode('utf-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -137,13 +137,13 @@ def jump_back(self, key=None, db=None): path = '/rpc/cur_jump_back' if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path = '%s?DB=%s' % (path, db) request_dict = {} request_dict['CUR'] = self.cursor_id if key: - request_dict['key'] = key.encode('UTF-8') + request_dict['key'] = key.encode('utf-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -256,7 +256,7 @@ def get_key(self, step=False): return False self.err.set_success() - return _tsv_to_dict(body, res.getheader('Content-Type', ''))[b'key'].decode('UTF-8') + return _tsv_to_dict(body, res.getheader('Content-Type', ''))[b'key'].decode('utf-8') def get_value(self, step=False): '''Get the value for the current record.''' @@ -302,7 +302,7 @@ def get(self, step=False): return False, False res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - key = res_dict[b'key'].decode('UTF-8') + key = res_dict[b'key'].decode('utf-8') value = self.unpack(res_dict[b'value']) self.err.set_success() @@ -326,7 +326,7 @@ def seize(self): res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) seize_dict = {} - seize_dict['key'] = res_dict[b'key'].decode('UTF-8') + seize_dict['key'] = res_dict[b'key'].decode('utf-8') seize_dict['value'] = self.unpack(res_dict[b'value']) self.err.set_success() @@ -363,12 +363,12 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.unpack = lambda data: pickle.loads(data) elif pack_type == KT_PACKER_JSON: - self.pack = lambda data: json.dumps(data).encode('UTF-8') - self.unpack = lambda data: json.loads(data.decode('UTF-8')) + self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') + self.unpack = lambda data: json.loads(data.decode('utf-8')) elif pack_type == KT_PACKER_STRING: - self.pack = lambda data: data.encode('UTF-8') - self.unpack = lambda data: data.decode('UTF-8') + self.pack = lambda data: data.encode('utf-8') + self.unpack = lambda data: data.decode('utf-8') else: raise Exception('unsupported pack type specified') @@ -424,9 +424,9 @@ def get(self, key, db=None): if key is None: return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('GET', key) @@ -548,7 +548,7 @@ def get_bulk(self, keys, atomic, db): for k, v in res_dict.items(): if v is not None: - rv[k.decode('UTF-8')[1:]] = self.unpack(v) + rv[k.decode('utf-8')[1:]] = self.unpack(v) self.err.set_success() return rv @@ -558,9 +558,9 @@ def get_int(self, key, db=None): self.err.set_error(self.err.LOGIC) return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('GET', key) @@ -621,7 +621,7 @@ def match_prefix(self, prefix, max, db): return [] for k, v in res_list: - rv.append(k.decode('UTF-8')[1:]) + rv.append(k.decode('utf-8')[1:]) self.err.set_success() return rv @@ -658,7 +658,7 @@ def match_regex(self, regex, max, db): return [] for k, v in res_list: - rv.append(k.decode('UTF-8')[1:]) + rv.append(k.decode('utf-8')[1:]) self.err.set_success() return rv @@ -668,9 +668,9 @@ def set(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -690,9 +690,9 @@ def add(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -740,9 +740,9 @@ def remove(self, key, db): self.err.set_error(self.err.LOGIC) return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('DELETE', key) @@ -760,9 +760,9 @@ def replace(self, key, value, expire, db): self.err.set_error(self.err.LOGIC) return False - key = quote(key.encode('UTF-8')) + key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('UTF-8')) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -802,9 +802,9 @@ def append(self, key, value, expire, db): if type(data) != type(value): if isinstance(data, bytes_type): - value = value.encode('UTF-8') + value = value.encode('utf-8') else: - value = value.decode('UTF-8') + value = value.decode('utf-8') data += value @@ -867,7 +867,7 @@ def report(self): res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) report_dict = {} for k, v in res_dict.items(): - report_dict[k.decode('UTF-8')] = v.decode('UTF-8') + report_dict[k.decode('utf-8')] = v.decode('utf-8') self.err.set_success() return report_dict @@ -888,7 +888,7 @@ def status(self, db=None): res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) status_dict = {} for k, v in res_dict.items(): - status_dict[k.decode('UTF-8')] = v.decode('UTF-8') + status_dict[k.decode('utf-8')] = v.decode('utf-8') self.err.set_success() return status_dict From 0e2bac9868d74836a40c2249cf19dda44b87e25c Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 1 Feb 2014 21:27:16 +0000 Subject: [PATCH 32/62] Added play_script method to both protocols. --- kyototycoon/kt_binary.py | 51 +++++++++++++++++++++++++++++++++++--- kyototycoon/kt_http.py | 43 ++++++++++++++++++++++++++++++-- kyototycoon/kyototycoon.py | 12 +++++++++ tests/t_script.py | 40 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 tests/t_script.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 1a2d438..42c2d09 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -33,6 +33,7 @@ MB_SET_BULK = 0xb8 MB_GET_BULK = 0xba MB_REMOVE_BULK = 0xb9 +MB_PLAY_SCRIPT = 0xb4 # Maximum signed 64bit integer... DEFAULT_EXPIRE = 0x7fffffffffffffff @@ -101,7 +102,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): for key, value in kv_dict.items(): if key is None: self.err.set_error(self.err.LOGIC) - return 0 + return False key = key.encode('utf-8') value = self.pack(value) @@ -128,7 +129,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): def remove_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): self.err.set_error(self.err.LOGIC) - return 0 + return False if db is None: db = 0 @@ -138,7 +139,7 @@ def remove_bulk(self, keys, atomic, db): for key in keys: if key is None: self.err.set_error(self.err.LOGIC) - return 0 + return False key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) @@ -172,7 +173,7 @@ def get_bulk(self, keys, atomic, db): for key in keys: if key is None: self.err.set_error(self.err.LOGIC) - return 0 + return False key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) @@ -257,6 +258,48 @@ def count(self, db=None): def size(self, db=None): raise NotImplementedError('supported under the HTTP procotol only') + def play_script(self, name, kv_dict=None): + if kv_dict and not isinstance(kv_dict, dict): + return False + + if kv_dict is None: + kv_dict = {} + + if name is None: + self.err.set_error(self.err.LOGIC) + return False + + name = name.encode('utf-8') + request = [struct.pack('!BIII', MB_PLAY_SCRIPT, 0, len(name), len(kv_dict)), name] + + for key, value in kv_dict.items(): + if not isinstance(value, bytes): + raise ValueError('value must be a byte sequence') + + key = key.encode('utf-8') + + request.append(struct.pack('!II', len(key), len(value))) + request.append(key) + request.append(value) + + self._write(b''.join(request)) + + magic, = struct.unpack('!B', self._read(1)) + if magic != MB_PLAY_SCRIPT: + self.err.set_error(self.err.INTERNAL) + return False + + num_items, = struct.unpack('!I', self._read(4)) + items = {} + for i in range(num_items): + key_length, value_length = struct.unpack('!II', self._read(8)) + key = self._read(key_length) + value = self._read(value_length) + items[key.decode('utf-8')] = value + + self.err.set_success() + return items + def _write(self, data): self.socket.sendall(data) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index efd4ed8..60a5152 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -478,7 +478,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): def remove_bulk(self, keys, atomic, db): if not hasattr(keys, '__iter__'): self.err.set_error(self.err.LOGIC) - return 0 + return False request_header = '' if atomic: @@ -489,7 +489,7 @@ def remove_bulk(self, keys, atomic, db): request_body += '_' + quote(key) + '\t\n' if len(request_body) < 1: self.err.set_error(self.err.LOGIC) - return 0 + return False path = '/rpc/remove_bulk' if db: @@ -921,6 +921,45 @@ def size(self, db=None): return None return int(st['size']) + def play_script(self, name, kv_dict=None): + if kv_dict and not isinstance(kv_dict, dict): + return False + + if kv_dict is None: + kv_dict = {} + + if name is None: + self.err.set_error(self.err.LOGIC) + return False + + path = '/rpc/play_script?name=' + quote(name) + request_body = '' + + for k, v in kv_dict.items(): + if not isinstance(v, bytes): + raise ValueError('value must be a byte sequence') + + k = quote(k.encode('utf-8')) + v = quote(v) + request_body += '_' + k + '\t' + v + '\n' + + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) + + res, body = self.getresponse() + if res.status != 200: + self.err.set_error(self.err.LOGIC) + return None + + rv = {} + res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) + + for k, v in res_dict.items(): + if v is not None: + rv[k.decode('utf-8')[1:]] = v + + self.err.set_success() + return rv + def _rest_put(self, operation, key, value, expire): headers = { 'X-Kt-Mode' : operation } if expire != None: diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 991440d..1170d47 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka # @@ -147,4 +148,15 @@ def cursor(self): return self.core.cursor() + def play_script(self, name, kv_dict=None): + ''' + Call a procedure of the scripting language extension. + + Because the input/output of server-side scripts may use a mix of formats, and unlike all + other methods, no implicit packing/unpacking is done to either input or output values. + + ''' + + return self.core.play_script(name, kv_dict) + # vim: set expandtab ts=4 sw=4 diff --git a/tests/t_script.py b/tests/t_script.py new file mode 100644 index 0000000..4cc5f81 --- /dev/null +++ b/tests/t_script.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. + +import config +import unittest +from kyototycoon import KyotoTycoon + +class UnitTest(unittest.TestCase): + def setUp(self): + self.kt_http_handle = KyotoTycoon(binary=False) + self.kt_http_handle.open() + + self.kt_bin_handle = KyotoTycoon(binary=True) + self.kt_bin_handle.open() + + def test_play_script(self): + self.assertTrue(self.kt_http_handle.clear()) + self.assertEqual(self.kt_http_handle.count(), 0) + + self.assertEqual(self.kt_bin_handle.play_script('echo', {'key1': b'abc'}), {'key1': b'abc'}) + + self.assertEqual(self.kt_bin_handle.play_script('set', {'key': b'key2', 'value': b'abcd'}), {}) + out = self.kt_bin_handle.play_script('get', {'key': b'key2'}) + self.assertTrue('value' in out) + self.assertEqual(out['value'], b'abcd') + + self.assertEqual(self.kt_http_handle.play_script('echo', {'key3': b'abc'}), {'key3': b'abc'}) + + self.assertEqual(self.kt_http_handle.play_script('set', {'key': b'key4', 'value': b'abcd'}), {}) + out = self.kt_http_handle.play_script('get', {'key': b'key4'}) + self.assertTrue('value' in out) + self.assertEqual(out['value'], b'abcd') + + +if __name__ == '__main__': + unittest.main() From 9ef48b1a21b24f8536be4130b1ab4a367d77020c Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 1 Feb 2014 21:35:55 +0000 Subject: [PATCH 33/62] More issues with encoding fixed. --- kyototycoon/kt_http.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 60a5152..65b0638 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -55,7 +55,7 @@ def _dict_to_tsv(dict): lines = [] for k, v in dict.items(): quoted = quote_from_bytes(v) if isinstance(v, bytes) else quote(str(v)) - lines.append(quote(k) + '\t' + quoted) + lines.append(quote(k.encode('utf-8')) + '\t' + quoted) return '\n'.join(lines) def _content_type_decoder(content_type=''): @@ -452,7 +452,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): path = '/rpc/set_bulk' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = '' @@ -461,7 +461,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): request_body = 'atomic\t\n' for k, v in kv_dict.items(): - k = quote(k) + k = quote(k.encode('utf-8')) v = quote(self.pack(v)) request_body += '_' + k + '\t' + v + '\n' @@ -486,14 +486,14 @@ def remove_bulk(self, keys, atomic, db): request_body = '' for key in keys: - request_body += '_' + quote(key) + '\t\n' + request_body += '_' + quote(key.encode('utf-8')) + '\t\n' if len(request_body) < 1: self.err.set_error(self.err.LOGIC) return False path = '/rpc/remove_bulk' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) @@ -521,7 +521,7 @@ def get_bulk(self, keys, atomic, db): request_body = '' for key in keys: - request_body += '_' + quote(key) + '\t\n' + request_body += '_' + quote(key.encode('utf-8')) + '\t\n' if len(request_body) < 1: self.err.set_error(self.err.LOGIC) @@ -529,7 +529,7 @@ def get_bulk(self, keys, atomic, db): path = '/rpc/get_bulk' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) @@ -577,7 +577,7 @@ def vacuum(self, db): path = '/rpc/vacuum' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('GET', path) @@ -876,7 +876,7 @@ def status(self, db=None): url = '/rpc/status' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) url += '?DB=' + db self.conn.request('GET', url) @@ -897,7 +897,7 @@ def clear(self, db=None): url = '/rpc/clear' if db: - db = quote(db) + db = db if isinstance(db, int) else quote(db.encode('utf-8')) url += '?DB=' + db self.conn.request('GET', url) From 30140412a711ba2a902a70cd3026b2509cdbe5e9 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 1 Feb 2014 22:47:08 +0000 Subject: [PATCH 34/62] Fix more encoding errors. Remove useless error checks (let it throw exceptions instead). --- kyototycoon/kt_binary.py | 49 ++-------- kyototycoon/kt_error.py | 2 +- kyototycoon/kt_http.py | 200 +++++++++++---------------------------- tests/t_simple.py | 12 --- tests/t_simple_binary.py | 8 -- 5 files changed, 65 insertions(+), 206 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 42c2d09..aa4506c 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -46,15 +46,12 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): if pack_type == KT_PACKER_PICKLE: self.pack = lambda data: pickle.dumps(data, pickle_protocol) self.unpack = lambda data: pickle.loads(data) - elif pack_type == KT_PACKER_JSON: self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') self.unpack = lambda data: json.loads(data.decode('utf-8')) - elif pack_type == KT_PACKER_STRING: self.pack = lambda data: data.encode('utf-8') self.unpack = lambda data: data.decode('utf-8') - else: raise Exception('unsupported pack type specified') @@ -74,9 +71,6 @@ def close(self): return True def get(self, key, db=None): - if key is None: - return False - values = self.get_bulk([key], True, db) if not values: return None @@ -84,12 +78,9 @@ def get(self, key, db=None): return values[key] def set_bulk(self, kv_dict, expire, atomic, db): - if not isinstance(kv_dict, dict): - return False - - if len(kv_dict) < 1: + if isinstance(kv_dict, dict) and len(kv_dict) < 1: self.err.set_error(self.err.LOGIC) - return False + return 0 if db is None: db = 0 @@ -100,10 +91,6 @@ def set_bulk(self, kv_dict, expire, atomic, db): request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] for key, value in kv_dict.items(): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = key.encode('utf-8') value = self.pack(value) @@ -119,17 +106,14 @@ def set_bulk(self, kv_dict, expire, atomic, db): return False num_items, = struct.unpack('!I', self._read(4)) - if num_items > 0: - self.err.set_success() - else: - self.err.set_error(self.err.NOTFOUND) + self.err.set_success() return num_items def remove_bulk(self, keys, atomic, db): - if not hasattr(keys, '__iter__'): + if len(keys) < 1: self.err.set_error(self.err.LOGIC) - return False + return 0 if db is None: db = 0 @@ -137,10 +121,6 @@ def remove_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_REMOVE_BULK, 0, len(keys))] for key in keys: - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -161,9 +141,9 @@ def remove_bulk(self, keys, atomic, db): return num_items def get_bulk(self, keys, atomic, db): - if not hasattr(keys, '__iter__'): + if len(keys) < 1: self.err.set_error(self.err.LOGIC) - return None + return {} if db is None: db = 0 @@ -171,10 +151,6 @@ def get_bulk(self, keys, atomic, db): request = [struct.pack('!BII', MB_GET_BULK, 0, len(keys))] for key in keys: - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = key.encode('utf-8') request.append(struct.pack('!HI', db, len(key))) request.append(key) @@ -224,10 +200,6 @@ def cas(self, key, old_val, new_val, expire, db): raise NotImplementedError('supported under the HTTP procotol only') def remove(self, key, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - numitems = self.remove_bulk([key], True, db) return numitems > 0 @@ -259,16 +231,9 @@ def size(self, db=None): raise NotImplementedError('supported under the HTTP procotol only') def play_script(self, name, kv_dict=None): - if kv_dict and not isinstance(kv_dict, dict): - return False - if kv_dict is None: kv_dict = {} - if name is None: - self.err.set_error(self.err.LOGIC) - return False - name = name.encode('utf-8') request = [struct.pack('!BIII', MB_PLAY_SCRIPT, 0, len(name), len(kv_dict)), name] diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index b09fe81..dead386 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -35,7 +35,7 @@ class KyotoTycoonError(object): INTERNAL: "Internal Error", NETWORK: "Network Error", NOTFOUND: "Record Not Found", - EMISC: "Miscellenious Error", + EMISC: "Miscellaneous Error", } def __init__(self): diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 65b0638..4259d7e 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -40,8 +40,6 @@ except ImportError: import json -# Stick with URL encoding for now. Eventually run a benchmark -# to evaluate what the most approariate encoding algorithm is. KT_HTTP_HEADER = { 'Content-Type' : 'text/tab-separated-values; colenc=U', } @@ -59,13 +57,15 @@ def _dict_to_tsv(dict): return '\n'.join(lines) def _content_type_decoder(content_type=''): - ''' Select the appropriate decoding function to use based on the response headers. ''' + '''Select the appropriate decoding function to use based on the response headers.''' + if content_type.endswith('colenc=B'): return base64.decodestring - elif content_type.endswith('colenc=U'): + + if content_type.endswith('colenc=U'): return unquote_to_bytes - else: - return lambda x: x + + return lambda x: x def _tsv_to_dict(tsv_str, content_type=''): decode = _content_type_decoder(content_type) @@ -116,8 +116,7 @@ def jump(self, key=None, db=None): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path = '%s?DB=%s' % (path, db) - request_dict = {} - request_dict['CUR'] = self.cursor_id + request_dict = {'CUR': self.cursor_id} if key: request_dict['key'] = key.encode('utf-8') @@ -140,8 +139,7 @@ def jump_back(self, key=None, db=None): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path = '%s?DB=%s' % (path, db) - request_dict = {} - request_dict['CUR'] = self.cursor_id + request_dict = {'CUR': self.cursor_id} if key: request_dict['key'] = key.encode('utf-8') @@ -161,9 +159,7 @@ def step(self): path = '/rpc/cur_step' - request_dict = {} - request_dict['CUR'] = self.cursor_id - + request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -180,9 +176,7 @@ def step_back(self): path = '/rpc/cur_step_back' - request_dict = {} - request_dict['CUR'] = self.cursor_id - + request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -199,9 +193,7 @@ def set_value(self, value, step=False, xt=None): path = '/rpc/cur_set_value' - request_dict = {} - request_dict['CUR'] = self.cursor_id - request_dict['value'] = self.pack(value) + request_dict = {'CUR': self.cursor_id, 'value': self.pack(value)} if step: request_dict['step'] = True if xt: @@ -223,9 +215,7 @@ def remove(self): path = '/rpc/cur_remove' - request_dict = {} - request_dict['CUR'] = self.cursor_id - + request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -242,8 +232,7 @@ def get_key(self, step=False): path = '/rpc/cur_get_key' - request_dict = {} - request_dict['CUR'] = self.cursor_id + request_dict = {'CUR': self.cursor_id} if step: request_dict['step'] = True @@ -263,8 +252,7 @@ def get_value(self, step=False): path = '/rpc/cur_get_value' - request_dict = {} - request_dict['CUR'] = self.cursor_id + request_dict = {'CUR': self.cursor_id} if step: request_dict['step'] = True @@ -284,8 +272,7 @@ def get(self, step=False): path = '/rpc/cur_get' - request_dict = {} - request_dict['CUR'] = self.cursor_id + request_dict = {'CUR': self.cursor_id} if step: request_dict['step'] = True @@ -313,9 +300,7 @@ def seize(self): path = '/rpc/cur_seize' - request_dict = {} - request_dict['CUR'] = self.cursor_id - + request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -325,9 +310,8 @@ def seize(self): return False res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) - seize_dict = {} - seize_dict['key'] = res_dict[b'key'].decode('utf-8') - seize_dict['value'] = self.unpack(res_dict[b'value']) + seize_dict = {'key': res_dict[b'key'].decode('utf-8'), + 'value': self.unpack(res_dict[b'value'])} self.err.set_success() return seize_dict @@ -337,9 +321,7 @@ def delete(self): path = '/rpc/cur_delete' - request_dict = {} - request_dict['CUR'] = self.cursor_id - + request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -352,7 +334,6 @@ def delete(self): return True - class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): self.err = kt_error.KyotoTycoonError() @@ -361,15 +342,12 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): if pack_type == KT_PACKER_PICKLE: self.pack = lambda data: pickle.dumps(data, pickle_protocol) self.unpack = lambda data: pickle.loads(data) - elif pack_type == KT_PACKER_JSON: self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') self.unpack = lambda data: json.loads(data.decode('utf-8')) - elif pack_type == KT_PACKER_STRING: self.pack = lambda data: data.encode('utf-8') self.unpack = lambda data: data.decode('utf-8') - else: raise Exception('unsupported pack type specified') @@ -386,17 +364,11 @@ def open(self, host, port, timeout): self.port = port self.timeout = timeout - try: - self.conn = httplib.HTTPConnection(host, port, timeout=timeout) - except Exception as e: - raise e + self.conn = httplib.HTTPConnection(host, port, timeout=timeout) return True def close(self): - try: - self.conn.close() - except Exception as e: - raise e + self.conn.close() return True def getresponse(self): @@ -421,9 +393,6 @@ def echo(self): return True def get(self, key, db=None): - if key is None: - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -435,6 +404,7 @@ def get(self, key, db=None): if res.status == 404: self.err.set_error(self.err.NOTFOUND) return None + if res.status != 200: self.err.set_error(self.err.EMISC) return False @@ -443,22 +413,16 @@ def get(self, key, db=None): return self.unpack(body) def set_bulk(self, kv_dict, expire, atomic, db): - if not isinstance(kv_dict, dict): - return False - - if len(kv_dict) < 1: + if isinstance(kv_dict, dict) and len(kv_dict) < 1: self.err.set_error(self.err.LOGIC) - return False + return 0 path = '/rpc/set_bulk' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - request_body = '' - - if atomic: - request_body = 'atomic\t\n' + request_body = 'atomic\t\n' if atomic else '' for k, v in kv_dict.items(): k = quote(k.encode('utf-8')) @@ -476,25 +440,21 @@ def set_bulk(self, kv_dict, expire, atomic, db): return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def remove_bulk(self, keys, atomic, db): - if not hasattr(keys, '__iter__'): + if len(keys) < 1: self.err.set_error(self.err.LOGIC) - return False - - request_header = '' - if atomic: - request_header = 'atomic\t\n' + return 0 + request_header = 'atomic\t\n' if atomic else '' request_body = '' + for key in keys: request_body += '_' + quote(key.encode('utf-8')) + '\t\n' - if len(request_body) < 1: - self.err.set_error(self.err.LOGIC) - return False path = '/rpc/remove_bulk' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db + self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() @@ -511,32 +471,27 @@ def remove_bulk(self, keys, atomic, db): return count def get_bulk(self, keys, atomic, db): - if not hasattr(keys, '__iter__'): + if len(keys) < 1: self.err.set_error(self.err.LOGIC) - return None - - request_header = '' - if atomic: - request_header = 'atomic\t\n' + return {} + request_header = 'atomic\t\n' if atomic else '' request_body = '' + for key in keys: request_body += '_' + quote(key.encode('utf-8')) + '\t\n' - if len(request_body) < 1: - self.err.set_error(self.err.LOGIC) - return {} - path = '/rpc/get_bulk' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db + self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) - return None + return False rv = {} res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) @@ -554,10 +509,6 @@ def get_bulk(self, keys, atomic, db): return rv def get_int(self, key, db=None): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -585,22 +536,20 @@ def vacuum(self, db): res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) + return False self.err.set_success() - return res.status == 200 + return True def match_prefix(self, prefix, max, db): - if prefix is None: - self.err.set_error(self.err.LOGIC) - return None - rv = [] - request_dict = {} - request_dict['prefix'] = prefix + request_dict = {'prefix': prefix.encode('utf-8')} if max: request_dict['max'] = max + if db: + db = db if isinstance(db, int) else quote(db.encode('utf-8')) request_dict['DB'] = db request_body = _dict_to_tsv(request_dict) @@ -633,9 +582,10 @@ def match_regex(self, regex, max, db): path = '/rpc/match_regex' if db: + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - request_dict = { 'regex': regex } + request_dict = {'regex': regex.encode('utf-8')} if max: request_dict['max'] = max @@ -645,7 +595,7 @@ def match_regex(self, regex, max, db): res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) - return None + return False rv = [] res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) @@ -664,19 +614,12 @@ def match_regex(self, regex, max, db): return rv def set(self, key, value, expire, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) - - self.err.set_success() - status = self._rest_put('set', key, value, expire) if status != 201: self.err.set_error(self.err.EMISC) @@ -686,10 +629,6 @@ def set(self, key, value, expire, db): return True def add(self, key, value, expire, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -697,7 +636,6 @@ def add(self, key, value, expire, db): value = self.pack(value) status = self._rest_put('add', key, value, expire) - if status != 201: self.err.set_error(self.err.EMISC) return False @@ -706,15 +644,12 @@ def add(self, key, value, expire, db): return True def cas(self, key, old_val, new_val, expire, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - path = '/rpc/cas' if db: - path += '?DB=' + db + db = db if isinstance(db, int) else quote(db.encode('utf-8')) + key = '/%s/%s' % (db, key) - request_dict = { 'key': key } + request_dict = {'key': key.encode('utf-8')} if old_val: request_dict['oval'] = self.pack(old_val) @@ -736,10 +671,6 @@ def cas(self, key, old_val, new_val, expire, db): return True def remove(self, key, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -756,10 +687,6 @@ def remove(self, key, db): return True def replace(self, key, value, expire, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - key = quote(key.encode('utf-8')) if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -767,7 +694,6 @@ def replace(self, key, value, expire, db): value = self.pack(value) status = self._rest_put('replace', key, value, expire) - if status != 201: self.err.set_error(self.err.NOTFOUND) return False @@ -776,10 +702,6 @@ def replace(self, key, value, expire, db): return True def append(self, key, value, expire, db): - self.err.set_error(self.err.LOGIC) - if key is None: - return False - # Simultaneous support for Python 2/3 makes this cumbersome... if sys.version_info[0] >= 3: bytes_type = bytes @@ -790,7 +712,7 @@ def append(self, key, value, expire, db): if (not isinstance(value, bytes_type) and not isinstance(value, unicode_type)): - return False + raise ValueError('value is not a string or bytes type') data = self.get(key) if data is None: @@ -798,6 +720,7 @@ def append(self, key, value, expire, db): if (not isinstance(data, bytes_type) and not isinstance(data, unicode_type)): + self.err.set_error(self.err.EMISC) return False if type(data) != type(value): @@ -808,20 +731,17 @@ def append(self, key, value, expire, db): data += value - if self.set(key, data, expire, db) is True: - self.err.set_success() - return True + if self.set(key, data, expire, db) is not True: + self.err.set_error(self.err.EMISC) + return False - self.err.set_error(self.err.EMISC) - return False + self.err.set_success() + return True def increment(self, key, delta, expire, db): - if key is None: - self.err.set_error(self.err.LOGIC) - return False - path = '/rpc/increment' if db: + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db delta = int(delta) @@ -843,6 +763,7 @@ def increment_double(self, key, delta, expire, db): path = '/rpc/increment_double' if db: + db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db delta = float(delta) @@ -922,19 +843,12 @@ def size(self, db=None): return int(st['size']) def play_script(self, name, kv_dict=None): - if kv_dict and not isinstance(kv_dict, dict): - return False - if kv_dict is None: kv_dict = {} - if name is None: - self.err.set_error(self.err.LOGIC) - return False + path = '/rpc/play_script?name=' + quote(name.encode('utf-8')) - path = '/rpc/play_script?name=' + quote(name) request_body = '' - for k, v in kv_dict.items(): if not isinstance(v, bytes): raise ValueError('value must be a byte sequence') diff --git a/tests/t_simple.py b/tests/t_simple.py index 2b3b53b..d3dbc91 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -45,12 +45,6 @@ def test_set(self): self.assertTrue(self.kt_handle.set('https://github.com', 'url2')) self.assertTrue(self.kt_handle.set('https://github.com/blog/', 'url3')) - self.assertFalse(self.kt_handle.set(None, 'value')) - self.assertEqual(error.code(), error.LOGIC) - - self.assertFalse(self.kt_handle.get(None)) - self.assertEqual(error.code(), error.LOGIC) - self.assertEqual(self.kt_handle.get('non_existent'), None) self.assertEqual(error.code(), error.NOTFOUND) @@ -95,8 +89,6 @@ def test_remove(self): self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.remove('deleteable key')) self.assertEqual(error.code(), error.SUCCESS) - self.assertFalse(self.kt_handle.remove(None)) - self.assertEqual(error.code(), error.LOGIC) def test_replace(self): self.assertTrue(self.kt_handle.clear()) @@ -113,8 +105,6 @@ def test_replace(self): self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('apple'), 'apfel') self.assertEqual(error.code(), error.SUCCESS) - self.assertFalse(self.kt_handle.replace(None, 'value')) - self.assertEqual(error.code(), error.LOGIC) self.assertTrue(self.kt_handle.replace('apple', 212)) self.assertEqual(error.code(), error.SUCCESS) @@ -135,7 +125,6 @@ def test_append(self): self.assertEqual(self.kt_handle.get('k2'), 'new val') self.assertTrue(self.kt_handle.set('k3', 777)) - self.assertFalse(self.kt_handle.append('k3', 111)) self.assertTrue(self.kt_handle.set('k4', b'abc')) self.assertTrue(self.kt_handle.append('k4', 'abc')) @@ -157,7 +146,6 @@ def test_add(self): self.assertTrue(self.kt_handle.add('lois', 'griffin')) self.assertTrue(self.kt_handle.add('seth', 'green')) self.assertTrue(self.kt_handle.add('nyc', 'new york city')) - self.assertFalse(self.kt_handle.add(None, 'value')) self.assertTrue(self.kt_handle.add('number', 111)) self.assertEqual(self.kt_handle.get('number'), 111) diff --git a/tests/t_simple_binary.py b/tests/t_simple_binary.py index 0a35568..a1e7fc4 100644 --- a/tests/t_simple_binary.py +++ b/tests/t_simple_binary.py @@ -49,12 +49,6 @@ def test_set(self): self.assertTrue(self.kt_handle.set('https://github.com', 'url2')) self.assertTrue(self.kt_handle.set('https://github.com/blog/', 'url3')) - self.assertFalse(self.kt_handle.set(None, 'value')) - self.assertEqual(error.code(), error.LOGIC) - - self.assertFalse(self.kt_handle.get(None)) - self.assertEqual(error.code(), error.LOGIC) - self.assertEqual(self.kt_handle.get('non_existent'), None) self.assertEqual(error.code(), error.NOTFOUND) @@ -76,8 +70,6 @@ def test_remove(self): self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.remove('deleteable key')) self.assertEqual(error.code(), error.SUCCESS) - self.assertFalse(self.kt_handle.remove(None)) - self.assertEqual(error.code(), error.LOGIC) def test_set_bulk(self): self.assertTrue(self.kt_handle_http.clear()) From 008dd764fb5dd687520a010f3c8c54edf3ec3a67 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 2 Feb 2014 00:12:47 +0000 Subject: [PATCH 35/62] Match expires behavior in the binary protocol with HTTP. Cleanups. --- kyototycoon/kt_binary.py | 28 +++------- kyototycoon/kt_http.py | 108 +++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 80 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index aa4506c..c7f6af7 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -12,6 +12,7 @@ import socket import struct +import time from . import kt_error @@ -72,31 +73,24 @@ def close(self): def get(self, key, db=None): values = self.get_bulk([key], True, db) - if not values: - return None - - return values[key] + return values[key] if values else None def set_bulk(self, kv_dict, expire, atomic, db): if isinstance(kv_dict, dict) and len(kv_dict) < 1: self.err.set_error(self.err.LOGIC) return 0 + expire = DEFAULT_EXPIRE if expire is None else int(time.time()) + expire + if db is None: db = 0 - if expire is None: - expire = DEFAULT_EXPIRE - request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] for key, value in kv_dict.items(): key = key.encode('utf-8') value = self.pack(value) - - request.append(struct.pack('!HIIq', db, len(key), len(value), expire)) - request.append(key) - request.append(value) + request.extend([struct.pack('!HIIq', db, len(key), len(value), expire), key, value]) self._write(b''.join(request)) @@ -106,7 +100,6 @@ def set_bulk(self, kv_dict, expire, atomic, db): return False num_items, = struct.unpack('!I', self._read(4)) - self.err.set_success() return num_items @@ -122,8 +115,7 @@ def remove_bulk(self, keys, atomic, db): for key in keys: key = key.encode('utf-8') - request.append(struct.pack('!HI', db, len(key))) - request.append(key) + request.extend([struct.pack('!HI', db, len(key)), key]) self._write(b''.join(request)) @@ -152,8 +144,7 @@ def get_bulk(self, keys, atomic, db): for key in keys: key = key.encode('utf-8') - request.append(struct.pack('!HI', db, len(key))) - request.append(key) + request.extend([struct.pack('!HI', db, len(key)), key]) self._write(b''.join(request)) @@ -242,10 +233,7 @@ def play_script(self, name, kv_dict=None): raise ValueError('value must be a byte sequence') key = key.encode('utf-8') - - request.append(struct.pack('!II', len(key), len(value))) - request.append(key) - request.append(value) + request.extend([struct.pack('!II', len(key), len(value)), key, value]) self._write(b''.join(request)) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 4259d7e..8390cd2 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -40,9 +40,7 @@ except ImportError: import json -KT_HTTP_HEADER = { - 'Content-Type' : 'text/tab-separated-values; colenc=U', -} +KT_HTTP_HEADER = { 'Content-Type' : 'text/tab-separated-values; colenc=U' } KT_PACKER_CUSTOM = 0 KT_PACKER_PICKLE = 1 @@ -53,7 +51,7 @@ def _dict_to_tsv(dict): lines = [] for k, v in dict.items(): quoted = quote_from_bytes(v) if isinstance(v, bytes) else quote(str(v)) - lines.append(quote(k.encode('utf-8')) + '\t' + quoted) + lines.append('%s\t%s' % (quote(k.encode('utf-8')), quoted)) return '\n'.join(lines) def _content_type_decoder(content_type=''): @@ -105,7 +103,7 @@ def __enter__(self): return self def __exit__(self, type, value, tb): - # Cleanup the cursor when leaving "with" blocks + # Cleanup the cursor when leaving "with" blocks... self.delete() def jump(self, key=None, db=None): @@ -158,7 +156,6 @@ def step(self): '''Step the cursor to the next record.''' path = '/rpc/cur_step' - request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -175,7 +172,6 @@ def step_back(self): '''Step the cursor to the previous record.''' path = '/rpc/cur_step_back' - request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -188,16 +184,17 @@ def step_back(self): self.err.set_success() return True - def set_value(self, value, step=False, xt=None): + def set_value(self, value, step=False, expire=None): '''Set the value for the current record.''' path = '/rpc/cur_set_value' - request_dict = {'CUR': self.cursor_id, 'value': self.pack(value)} + if step: request_dict['step'] = True - if xt: - request_dict['xt'] = xt + + if expire: + request_dict['xt'] = expire request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -214,7 +211,6 @@ def remove(self): '''Remove the current record.''' path = '/rpc/cur_remove' - request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -231,8 +227,8 @@ def get_key(self, step=False): '''Get the key for the current record.''' path = '/rpc/cur_get_key' - request_dict = {'CUR': self.cursor_id} + if step: request_dict['step'] = True @@ -251,8 +247,8 @@ def get_value(self, step=False): '''Get the value for the current record.''' path = '/rpc/cur_get_value' - request_dict = {'CUR': self.cursor_id} + if step: request_dict['step'] = True @@ -271,8 +267,8 @@ def get(self, step=False): '''Get a (key,value) pair for the current record.''' path = '/rpc/cur_get' - request_dict = {'CUR': self.cursor_id} + if step: request_dict['step'] = True @@ -299,7 +295,6 @@ def seize(self): '''Get a (key,value) pair for the current record, and remove it atomically.''' path = '/rpc/cur_seize' - request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -320,7 +315,6 @@ def delete(self): '''Delete the cursor.''' path = '/rpc/cur_delete' - request_dict = {'CUR': self.cursor_id} request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -422,14 +416,14 @@ def set_bulk(self, kv_dict, expire, atomic, db): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - request_body = 'atomic\t\n' if atomic else '' + request_body = ['atomic\t\n' if atomic else ''] for k, v in kv_dict.items(): k = quote(k.encode('utf-8')) v = quote(self.pack(v)) - request_body += '_' + k + '\t' + v + '\n' + request_body.append('_%s\t%s\n' % (k, v)) - self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=''.join(request_body), headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -444,18 +438,17 @@ def remove_bulk(self, keys, atomic, db): self.err.set_error(self.err.LOGIC) return 0 - request_header = 'atomic\t\n' if atomic else '' - request_body = '' - - for key in keys: - request_body += '_' + quote(key.encode('utf-8')) + '\t\n' - path = '/rpc/remove_bulk' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) + request_body = ['atomic\t\n' if atomic else ''] + + for key in keys: + request_body.append('_%s\t\n' % quote(key.encode('utf-8'))) + + self.conn.request('POST', path, body=''.join(request_body), headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -475,18 +468,17 @@ def get_bulk(self, keys, atomic, db): self.err.set_error(self.err.LOGIC) return {} - request_header = 'atomic\t\n' if atomic else '' - request_body = '' - - for key in keys: - request_body += '_' + quote(key.encode('utf-8')) + '\t\n' - path = '/rpc/get_bulk' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - self.conn.request('POST', path, body=request_header + request_body, headers=KT_HTTP_HEADER) + request_body = ['atomic\t\n' if atomic else ''] + + for key in keys: + request_body.append('_%s\t\n' % quote(key.encode('utf-8'))) + + self.conn.request('POST', path, body=''.join(request_body), headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -526,7 +518,6 @@ def get_int(self, key, db=None): def vacuum(self, db): path = '/rpc/vacuum' - if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db @@ -644,6 +635,10 @@ def add(self, key, value, expire, db): return True def cas(self, key, old_val, new_val, expire, db): + if old_val is None and new_val is None: + self.err.set_error(self.err.LOGIC) + return False + path = '/rpc/cas' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) @@ -651,10 +646,12 @@ def cas(self, key, old_val, new_val, expire, db): request_dict = {'key': key.encode('utf-8')} - if old_val: + if old_val is not None: request_dict['oval'] = self.pack(old_val) - if new_val: + + if new_val is not None: request_dict['nval'] = self.pack(new_val) + if expire: request_dict['xt'] = expire @@ -744,7 +741,6 @@ def increment(self, key, delta, expire, db): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - delta = int(delta) request_body = 'key\t%s\nnum\t%d\n' % (key, delta) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -766,7 +762,6 @@ def increment_double(self, key, delta, expire, db): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - delta = float(delta) request_body = 'key\t%s\nnum\t%f\n' % (key, delta) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -794,13 +789,12 @@ def report(self): return report_dict def status(self, db=None): - url = '/rpc/status' - + path = '/rpc/status' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - url += '?DB=' + db + path += '?DB=' + db - self.conn.request('GET', url) + self.conn.request('GET', path) res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) @@ -815,13 +809,12 @@ def status(self, db=None): return status_dict def clear(self, db=None): - url = '/rpc/clear' - + path = '/rpc/clear' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - url += '?DB=' + db + path += '?DB=' + db - self.conn.request('GET', url) + self.conn.request('GET', path) res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) @@ -832,15 +825,11 @@ def clear(self, db=None): def count(self, db=None): st = self.status(db) - if st is None: - return None - return int(st['count']) + return None if st is None else int(st['count']) def size(self, db=None): st = self.status(db) - if st is None: - return None - return int(st['size']) + return None if st is None else int(st['size']) def play_script(self, name, kv_dict=None): if kv_dict is None: @@ -848,16 +837,16 @@ def play_script(self, name, kv_dict=None): path = '/rpc/play_script?name=' + quote(name.encode('utf-8')) - request_body = '' + request_body = [] for k, v in kv_dict.items(): if not isinstance(v, bytes): raise ValueError('value must be a byte sequence') k = quote(k.encode('utf-8')) v = quote(v) - request_body += '_' + k + '\t' + v + '\n' + request_body.append('_%s\t%s\n' % (k, v)) - self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=''.join(request_body), headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: @@ -875,10 +864,9 @@ def play_script(self, name, kv_dict=None): return rv def _rest_put(self, operation, key, value, expire): - headers = { 'X-Kt-Mode' : operation } - if expire != None: - expire = int(time.time()) + expire; - headers["X-Kt-Xt"] = str(expire) + headers = {'X-Kt-Mode' : operation} + if expire is not None: + headers["X-Kt-Xt"] = str(int(time.time()) + expire) self.conn.request('PUT', key, value, headers) res, body = self.getresponse() From 60b1240c3e08ac5736a4a0050d3d54249401b330 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 2 Feb 2014 01:04:05 +0000 Subject: [PATCH 36/62] Fixed expiration with the binary protocol. Fixed cas with multiple databases. --- kyototycoon/kt_binary.py | 4 ++-- kyototycoon/kt_http.py | 4 ++-- tests/t_expire.py | 35 +++++++++++++++++++++-------------- tests/t_multi.py | 18 +++++++++--------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index c7f6af7..ffc2000 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -12,7 +12,6 @@ import socket import struct -import time from . import kt_error @@ -80,7 +79,8 @@ def set_bulk(self, kv_dict, expire, atomic, db): self.err.set_error(self.err.LOGIC) return 0 - expire = DEFAULT_EXPIRE if expire is None else int(time.time()) + expire + if expire is None: + expire = DEFAULT_EXPIRE if db is None: db = 0 diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 8390cd2..eee2386 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -642,9 +642,9 @@ def cas(self, key, old_val, new_val, expire, db): path = '/rpc/cas' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + path += '?DB=' + db - request_dict = {'key': key.encode('utf-8')} + request_dict = {'key': quote(key.encode('utf-8'))} if old_val is not None: request_dict['oval'] = self.pack(old_val) diff --git a/tests/t_expire.py b/tests/t_expire.py index e27e1ac..24e3d37 100644 --- a/tests/t_expire.py +++ b/tests/t_expire.py @@ -12,31 +12,38 @@ class UnitTest(unittest.TestCase): def setUp(self): - self.kt_handle = KyotoTycoon() - self.kt_handle.open() + self.kt_http_handle = KyotoTycoon(binary=False) + self.kt_http_handle.open() + + self.kt_bin_handle = KyotoTycoon(binary=True) + self.kt_bin_handle.open() + self.LARGE_KEY_LEN = 8000 def test_set_expire(self): - self.assertTrue(self.kt_handle.clear()) + self.assertTrue(self.kt_http_handle.clear()) # Set record to be expired in 2 seconds. - self.assertTrue(self.kt_handle.set('key', 'value', 2)) - self.assertEqual(self.kt_handle.get('key'), 'value') - self.assertEqual(self.kt_handle.count(), 1) + self.assertTrue(self.kt_http_handle.set('key1', 'value', 2)) + self.assertEqual(self.kt_http_handle.get('key1'), 'value') + self.assertEqual(self.kt_http_handle.count(), 1) + time.sleep(3) + self.assertEqual(self.kt_http_handle.count(), 0) - # Must be expired after 3 seconds. + # Set record to be expired in 2 seconds. + self.assertTrue(self.kt_bin_handle.set('key2', 'value', 2)) + self.assertEqual(self.kt_bin_handle.get('key2'), 'value') + self.assertEqual(self.kt_http_handle.count(), 1) time.sleep(3) - self.assertEqual(self.kt_handle.get('key'), None) - self.assertEqual(self.kt_handle.count(), 0) + self.assertEqual(self.kt_http_handle.count(), 0) def test_add_expire(self): - self.assertTrue(self.kt_handle.clear()) - - self.assertTrue(self.kt_handle.add('hello', 'world', 2)) - self.assertEqual(self.kt_handle.get('hello'), 'world') + self.assertTrue(self.kt_http_handle.clear()) + self.assertTrue(self.kt_http_handle.add('hello', 'world', 2)) + self.assertEqual(self.kt_http_handle.get('hello'), 'world') time.sleep(3) - self.assertEqual(self.kt_handle.get('hello'), None) + self.assertEqual(self.kt_http_handle.get('hello'), None) if __name__ == '__main__': unittest.main() diff --git a/tests/t_multi.py b/tests/t_multi.py index 5fb4286..b18df75 100644 --- a/tests/t_multi.py +++ b/tests/t_multi.py @@ -46,9 +46,9 @@ def test_set_get(self): self.assertTrue(self.kt_handle.set('ice', 'cream', db=DB_2)) self.assertFalse(self.kt_handle.set('palo', 'alto', db=DB_INVALID)) - assert self.kt_handle.get('ice') is None - assert self.kt_handle.get('ice', db=DB_1) is None - assert self.kt_handle.get('ice', db=DB_INVALID) is None + self.assertEqual(self.kt_handle.get('ice'), None) + self.assertEqual(self.kt_handle.get('ice', db=DB_1), None) + self.assertFalse(self.kt_handle.get('ice', db=DB_INVALID)) self.assertEqual(self.kt_handle.get('ice', db='two.kch'), 'cream') self.assertEqual(self.kt_handle.count(db=DB_1), 0) @@ -59,8 +59,8 @@ def test_set_get(self): self.assertEqual(self.kt_handle.get('frozen'), 'yoghurt') self.assertEqual(self.kt_handle.get('frozen', db=DB_1), 'yoghurt') - assert self.kt_handle.get('frozen', db=DB_2) is None - assert self.kt_handle.get('frozen', db=DB_INVALID) is None + self.assertEqual(self.kt_handle.get('frozen', db=DB_2), None) + self.assertFalse(self.kt_handle.get('frozen', db=DB_INVALID), None) self.assertTrue(self.kt_handle.clear(db=DB_1)) self.assertEqual(self.kt_handle.count(db=DB_1), 0) @@ -120,10 +120,10 @@ def test_replace(self): def test_cas(self): self.assertTrue(self.clear_all()) self.assertTrue(self.kt_handle.set('key', 'xxx')) - self.assertFalse(self.kt_handle.cas('key', old_val='xxx', - new_val='yyy', db=DB_2)) - self.assertTrue(self.kt_handle.cas('key', old_val='xxx', - new_val='yyy', db=DB_1)) + self.assertEqual(self.kt_handle.get('key', db=DB_2), None) + self.assertFalse(self.kt_handle.cas('key', old_val='xxx', new_val='yyy', db=DB_2)) + self.assertEqual(self.kt_handle.get('key', db=DB_1), 'xxx') + self.assertTrue(self.kt_handle.cas('key', old_val='xxx', new_val='yyy', db=DB_1)) self.assertTrue(self.kt_handle.cas('key', new_val='xxx', db=DB_2)) def test_remove(self): From 4ac8b5f24751d7732e5c0f2952d657b7af0af0e1 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 2 Feb 2014 01:12:19 +0000 Subject: [PATCH 37/62] Update README for play_script. Bump version. --- README | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README b/README index c9d53c3..959704d 100644 --- a/README +++ b/README @@ -25,6 +25,7 @@ to 6x, but only the following operations are available: * ``get()`` and ``get_bulk()`` * ``set()`` and ``set_bulk()`` * ``remove()`` and ``remove_bulk()`` + * ``play_script()`` Other operations will throw a ``NotImplementedError`` exception. diff --git a/setup.py b/setup.py index c750feb..c3337f5 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.2', + version='0.5.5', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], From e88dc280db25ab2f6bfa4132d251a3e09e8775e1 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 2 Feb 2014 01:45:05 +0000 Subject: [PATCH 38/62] Fixed two more elusive HTTP key quoting issues. --- kyototycoon/kt_http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index eee2386..2d07694 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -40,7 +40,7 @@ except ImportError: import json -KT_HTTP_HEADER = { 'Content-Type' : 'text/tab-separated-values; colenc=U' } +KT_HTTP_HEADER = {'Content-Type' : 'text/tab-separated-values; colenc=U'} KT_PACKER_CUSTOM = 0 KT_PACKER_PICKLE = 1 @@ -116,7 +116,7 @@ def jump(self, key=None, db=None): request_dict = {'CUR': self.cursor_id} if key: - request_dict['key'] = key.encode('utf-8') + request_dict['key'] = quote(key.encode('utf-8')) request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -139,7 +139,7 @@ def jump_back(self, key=None, db=None): request_dict = {'CUR': self.cursor_id} if key: - request_dict['key'] = key.encode('utf-8') + request_dict['key'] = quote(key.encode('utf-8')) request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) From 20544586f39f02f6a4447c008b2504185a6507af Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 2 Feb 2014 02:19:16 +0000 Subject: [PATCH 39/62] Pass DB in path for match_prefix, for consistency. --- kyototycoon/kt_http.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 2d07694..acfabce 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -112,11 +112,11 @@ def jump(self, key=None, db=None): path = '/rpc/cur_jump' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - path = '%s?DB=%s' % (path, db) + path += '?DB=' + db request_dict = {'CUR': self.cursor_id} if key: - request_dict['key'] = quote(key.encode('utf-8')) + request_dict['key'] = key.encode('utf-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -135,11 +135,11 @@ def jump_back(self, key=None, db=None): path = '/rpc/cur_jump_back' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - path = '%s?DB=%s' % (path, db) + path += '?DB=' + db request_dict = {'CUR': self.cursor_id} if key: - request_dict['key'] = quote(key.encode('utf-8')) + request_dict['key'] = key.encode('utf-8') request_body = _dict_to_tsv(request_dict) self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -533,24 +533,28 @@ def vacuum(self, db): return True def match_prefix(self, prefix, max, db): - rv = [] - request_dict = {'prefix': prefix.encode('utf-8')} - - if max: - request_dict['max'] = max + if prefix is None: + self.err.set_error(self.err.LOGIC) + return None + path = '/rpc/match_prefix' if db: db = db if isinstance(db, int) else quote(db.encode('utf-8')) - request_dict['DB'] = db + path += '?DB=' + db + + request_dict = {'prefix': prefix.encode('utf-8')} + if max: + request_dict['max'] = max request_body = _dict_to_tsv(request_dict) - self.conn.request('POST', '/rpc/match_prefix', body=request_body, headers=KT_HTTP_HEADER) + self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.getresponse() if res.status != 200: self.err.set_error(self.err.EMISC) return False + rv = [] res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) if len(res_list) == 0 or res_list[-1][0] != b'num': self.err.set_error(self.err.EMISC) @@ -644,7 +648,7 @@ def cas(self, key, old_val, new_val, expire, db): db = db if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db - request_dict = {'key': quote(key.encode('utf-8'))} + request_dict = {'key': key.encode('utf-8')} if old_val is not None: request_dict['oval'] = self.pack(old_val) From b53a6fb45934856fcf1aca419b4022241fc7fcbc Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Mon, 3 Feb 2014 12:19:18 +0000 Subject: [PATCH 40/62] Exclude the play script test case from the default test suite. --- tests/t_all.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/t_all.py b/tests/t_all.py index 9087630..e167da6 100644 --- a/tests/t_all.py +++ b/tests/t_all.py @@ -8,6 +8,8 @@ # USAGE: # $ python t_all.py # $ python t_all.py ExpireTestCase +# $ python t_all.py MultiTestCase +# $ python t_all.py ScriptTestCase import os import re @@ -24,7 +26,7 @@ def _run_all_tests(): match = _TEST_MODULE_PATTERN.search(filename) if match: case = match.group(1) - if case != 't_expire' and case != 't_multi': + if case not in ('t_expire', 't_multi', 't_script'): module_names.append(case) return loader.loadTestsFromNames(module_names) @@ -33,5 +35,13 @@ def ExpireTestCase(): loader = unittest.TestLoader() return loader.loadTestsFromName('t_expire') +def MultiTestCase(): + loader = unittest.TestLoader() + return loader.loadTestsFromName('t_multi') + +def ScriptTestCase(): + loader = unittest.TestLoader() + return loader.loadTestsFromName('t_script') + if __name__ == '__main__': unittest.main(defaultTest='_run_all_tests') From bca3a23d54741c990ee8c50a3033613f66b5c7db Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 5 Feb 2014 14:23:08 +0000 Subject: [PATCH 41/62] Support for bytes (raw) and custom packers. --- kyototycoon/kt_binary.py | 20 +++++++++++++- kyototycoon/kt_http.py | 20 +++++++++++++- tests/t_packer_bytes.py | 48 ++++++++++++++++++++++++++++++++++ tests/t_packer_custom.py | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/t_packer_bytes.py create mode 100644 tests/t_packer_custom.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index ffc2000..55319d2 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -29,6 +29,7 @@ KT_PACKER_PICKLE = 1 KT_PACKER_JSON = 2 KT_PACKER_STRING = 3 +KT_PACKER_BYTES = 4 MB_SET_BULK = 0xb8 MB_GET_BULK = 0xba @@ -39,19 +40,36 @@ DEFAULT_EXPIRE = 0x7fffffffffffffff class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): + def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer=None): self.err = kt_error.KyotoTycoonError() self.socket = None + if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: + raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') + if pack_type == KT_PACKER_PICKLE: self.pack = lambda data: pickle.dumps(data, pickle_protocol) self.unpack = lambda data: pickle.loads(data) + elif pack_type == KT_PACKER_JSON: self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') self.unpack = lambda data: json.loads(data.decode('utf-8')) + elif pack_type == KT_PACKER_STRING: self.pack = lambda data: data.encode('utf-8') self.unpack = lambda data: data.decode('utf-8') + + elif pack_type == KT_PACKER_BYTES: + self.pack = lambda data: data + self.unpack = lambda data: data + + elif pack_type == KT_PACKER_CUSTOM: + if custom_packer is None: + raise Exception('"KT_PACKER_CUSTOM" requires a packer object') + + self.pack = custom_packer.pack + self.unpack = custom_packer.unpack + else: raise Exception('unsupported pack type specified') diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index acfabce..4bfe6c3 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -46,6 +46,7 @@ KT_PACKER_PICKLE = 1 KT_PACKER_JSON = 2 KT_PACKER_STRING = 3 +KT_PACKER_BYTES = 4 def _dict_to_tsv(dict): lines = [] @@ -329,19 +330,36 @@ def delete(self): class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2): + def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer=None): self.err = kt_error.KyotoTycoonError() self.pack_type = pack_type + if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: + raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') + if pack_type == KT_PACKER_PICKLE: self.pack = lambda data: pickle.dumps(data, pickle_protocol) self.unpack = lambda data: pickle.loads(data) + elif pack_type == KT_PACKER_JSON: self.pack = lambda data: json.dumps(data, separators=(',',':')).encode('utf-8') self.unpack = lambda data: json.loads(data.decode('utf-8')) + elif pack_type == KT_PACKER_STRING: self.pack = lambda data: data.encode('utf-8') self.unpack = lambda data: data.decode('utf-8') + + elif pack_type == KT_PACKER_BYTES: + self.pack = lambda data: data + self.unpack = lambda data: data + + elif pack_type == KT_PACKER_CUSTOM: + if custom_packer is None: + raise Exception('"KT_PACKER_CUSTOM" requires a packer object') + + self.pack = custom_packer.pack + self.unpack = custom_packer.unpack + else: raise Exception('unsupported pack type specified') diff --git a/tests/t_packer_bytes.py b/tests/t_packer_bytes.py new file mode 100644 index 0000000..1211b89 --- /dev/null +++ b/tests/t_packer_bytes.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. + +import config +import unittest +from kyototycoon import KyotoTycoon + +import kyototycoon.kt_http as kt_http +import kyototycoon.kt_binary as kt_binary + +class UnitTest(unittest.TestCase): + def setUp(self): + self.kt_bin_handle = KyotoTycoon(binary=True, pack_type=kt_binary.KT_PACKER_BYTES) + self.kt_bin_handle.open() + + self.kt_http_handle = KyotoTycoon(binary=False, pack_type=kt_http.KT_PACKER_BYTES) + self.kt_http_handle.open() + + def test_packer_bytes(self): + self.assertTrue(self.kt_http_handle.clear()) + self.assertEqual(self.kt_http_handle.count(), 0) + + value1 = bytes([0x00, 0x01, 0x02, 0xff]) + self.assertTrue(self.kt_bin_handle.set('key1', value1)) + value2 = self.kt_bin_handle.get('key1') + self.assertEqual(value1, value2) + self.assertEqual(type(value1), type(value2)) + + value3 = bytes([0x00, 0x01, 0x03, 0xff]) + self.assertTrue(self.kt_http_handle.set('key2', value3)) + value4 = self.kt_http_handle.get('key2') + self.assertEqual(value3, value4) + self.assertEqual(type(value3), type(value4)) + + self.assertTrue(self.kt_http_handle.append('key2', 'xyz')) + self.assertEqual(self.kt_http_handle.get('key2'), value3 + b'xyz') + + self.assertEqual(self.kt_http_handle.count(), 2) + self.assertTrue(self.kt_http_handle.clear()) + self.assertEqual(self.kt_http_handle.count(), 0) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/t_packer_custom.py b/tests/t_packer_custom.py new file mode 100644 index 0000000..9705571 --- /dev/null +++ b/tests/t_packer_custom.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. + +import config +import unittest +from kyototycoon import KyotoTycoon + +import kyototycoon.kt_http as kt_http +import kyototycoon.kt_binary as kt_binary + +class CustomPacker(object): + prefix = '' + suffix = '' + + def pack(self, data): + data = self.prefix + data + self.suffix + return data.encode('utf-8') + + def unpack(self, data): + data = data[len(self.prefix):-len(self.suffix)] + return data.decode('utf-8') + +class UnitTest(unittest.TestCase): + def setUp(self): + self.kt_bin_handle = KyotoTycoon(binary=True, pack_type=kt_binary.KT_PACKER_CUSTOM, + custom_packer=CustomPacker()) + self.kt_bin_handle.open() + + self.kt_http_handle = KyotoTycoon(binary=False, pack_type=kt_http.KT_PACKER_CUSTOM, + custom_packer=CustomPacker()) + self.kt_http_handle.open() + + def test_packer_bytes(self): + self.assertTrue(self.kt_http_handle.clear()) + self.assertEqual(self.kt_http_handle.count(), 0) + + self.assertTrue(self.kt_bin_handle.set('key1', 'abc')) + self.assertEqual(self.kt_bin_handle.get('key1'), 'abc') + + self.assertTrue(self.kt_http_handle.set('key2', 'abcd')) + self.assertEqual(self.kt_http_handle.get('key2'), 'abcd') + + self.assertTrue(self.kt_http_handle.append('key2', 'xyz')) + self.assertEqual(self.kt_http_handle.get('key2'), 'abcdxyz') + + self.assertEqual(self.kt_http_handle.count(), 2) + self.assertTrue(self.kt_http_handle.clear()) + self.assertEqual(self.kt_http_handle.count(), 0) + +if __name__ == '__main__': + unittest.main() From 1885019926a8162dfec0044ddadf012e8d8463af Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 5 Feb 2014 15:04:32 +0000 Subject: [PATCH 42/62] Improved README. --- README | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/README b/README index 959704d..a14e78c 100644 --- a/README +++ b/README @@ -14,9 +14,9 @@ There is currently no documentation for this software but in the meantime please see the file "kyototycoon.py" for the interface. -This fork is backwards compatible, and includes cursor support -and significant performance improvements over the original -library. It also supports both Python 2.x and 3.x. +This fork includes cursor support and significant performance +improvements over the original library. It also supports unicode +keys and works with both Python 2.x and 3.x. The more efficient binary protocol is also supported along with the HTTP protocol. It provides a performance improvement of up @@ -27,7 +27,31 @@ to 6x, but only the following operations are available: * ``remove()`` and ``remove_bulk()`` * ``play_script()`` -Other operations will throw a ``NotImplementedError`` exception. +Other operations will throw a ``NotImplementedError`` exception +but, as a last resort, it's always possible to have two +KyotoTycoon objects open to the same server, one using HTTP and +the other using the binary protocol. + +The library does automatic packing and unpacking (marshalling) +of values coming from/to the database. The following data +storage formats are available by default: + + * ``KT_PACKER_PICKLE`` - Python "pickle" format. + * ``KT_PACKER_JSON`` - JSON format (compact representation). + * ``KT_PACKER_STRING`` - Strings (UTF-8). + * ``KT_PACKER_BYTES`` - Binary data. + +There is also a ``KT_PACKER_CUSTOM`` format available where you +can specify your own object to do the marshalling. This object +needs to provide the following two methods: + + * ``.pack(self, data)`` - convert "data" to ``bytes()`` + * ``.unpack(self, data)`` - convert "data" from ``bytes()`` + +Marshalling is done for all methods except ``play_script()``, +because the server can return data in more than one format at +once. The caller will most likely know the type of data that +the called script returns and must do the marshalling itself. INSTALLATION From 9f8dca30a732ae6cc59faf8f5303d41d70ece76b Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 9 Feb 2014 14:42:01 +0000 Subject: [PATCH 43/62] Removed "#!" line from the top of non-executable files and replace vim modelines at the bottom with a generic "# EOF - filename.py" comment. Move packer constants to a separate file to avoid duplication. Dropped the "picker_protocol" parameter, use a custom packer. Fix integer database IDs in the HTTP protocol. --- kyototycoon/__init__.py | 15 ++- kyototycoon/kt_binary.py | 20 ++-- kyototycoon/kt_common.py | 14 +++ kyototycoon/kt_error.py | 3 +- kyototycoon/kt_http.py | 58 ++++----- kyototycoon/kyototycoon.py | 18 ++- tests/kt_performance.py | 10 +- tests/t_all.py | 18 +-- tests/t_expire.py | 6 +- tests/t_multi.py | 238 +++++++++++++++++++++++-------------- tests/t_script.lua | 51 ++++++++ tests/t_script.py | 3 + 12 files changed, 295 insertions(+), 159 deletions(-) create mode 100644 kyototycoon/kt_common.py create mode 100644 tests/t_script.lua diff --git a/kyototycoon/__init__.py b/kyototycoon/__init__.py index 577edfa..58a3f1f 100644 --- a/kyototycoon/__init__.py +++ b/kyototycoon/__init__.py @@ -1,7 +1,14 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from .kyototycoon import KyotoTycoon -from .kyototycoon import KT_DEFAULT_HOST, KT_DEFAULT_PORT, KT_DEFAULT_TIMEOUT +from .kyototycoon import KyotoTycoon, \ + KT_DEFAULT_HOST, \ + KT_DEFAULT_PORT, \ + KT_DEFAULT_TIMEOUT -# vim: set expandtab ts=4 sw=4 +from .kt_common import KT_PACKER_CUSTOM, \ + KT_PACKER_PICKLE, \ + KT_PACKER_JSON, \ + KT_PACKER_STRING, \ + KT_PACKER_BYTES + +# EOF - __init__.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 55319d2..9d0c68d 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2013, Carlos Rodrigues @@ -15,6 +14,12 @@ from . import kt_error +from .kt_common import KT_PACKER_CUSTOM, \ + KT_PACKER_PICKLE, \ + KT_PACKER_JSON, \ + KT_PACKER_STRING, \ + KT_PACKER_BYTES + try: import cPickle as pickle except ImportError: @@ -25,12 +30,6 @@ except ImportError: import json -KT_PACKER_CUSTOM = 0 -KT_PACKER_PICKLE = 1 -KT_PACKER_JSON = 2 -KT_PACKER_STRING = 3 -KT_PACKER_BYTES = 4 - MB_SET_BULK = 0xb8 MB_GET_BULK = 0xba MB_REMOVE_BULK = 0xb9 @@ -40,7 +39,7 @@ DEFAULT_EXPIRE = 0x7fffffffffffffff class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer=None): + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): self.err = kt_error.KyotoTycoonError() self.socket = None @@ -48,7 +47,8 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer= raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') if pack_type == KT_PACKER_PICKLE: - self.pack = lambda data: pickle.dumps(data, pickle_protocol) + # Pickle protocol v2 is is used here instead of the default... + self.pack = lambda data: pickle.dumps(data, 2) self.unpack = lambda data: pickle.loads(data) elif pack_type == KT_PACKER_JSON: @@ -287,4 +287,4 @@ def _read(self, bytecnt): return b''.join(buf) -# vim: set expandtab ts=4 sw=4 +# EOF - kt_binary.py diff --git a/kyototycoon/kt_common.py b/kyototycoon/kt_common.py new file mode 100644 index 0000000..108fe06 --- /dev/null +++ b/kyototycoon/kt_common.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2011, Toru Maesaka +# +# Redistribution and use of this source code is licensed under +# the BSD license. See COPYING file for license description. + +KT_PACKER_CUSTOM = 0 +KT_PACKER_PICKLE = 1 +KT_PACKER_JSON = 2 +KT_PACKER_STRING = 3 +KT_PACKER_BYTES = 4 + +# EOF - kt_common.py diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index dead386..dacbae5 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka @@ -60,4 +59,4 @@ def name(self): def message(self): return self.error_message -# vim: set expandtab ts=4 sw=4 +# EOF - kt_error.py diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 4bfe6c3..cef6649 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka @@ -13,6 +12,12 @@ from . import kt_error +from .kt_common import KT_PACKER_CUSTOM, \ + KT_PACKER_PICKLE, \ + KT_PACKER_JSON, \ + KT_PACKER_STRING, \ + KT_PACKER_BYTES + try: import httplib except ImportError: @@ -42,12 +47,6 @@ KT_HTTP_HEADER = {'Content-Type' : 'text/tab-separated-values; colenc=U'} -KT_PACKER_CUSTOM = 0 -KT_PACKER_PICKLE = 1 -KT_PACKER_JSON = 2 -KT_PACKER_STRING = 3 -KT_PACKER_BYTES = 4 - def _dict_to_tsv(dict): lines = [] for k, v in dict.items(): @@ -112,7 +111,7 @@ def jump(self, key=None, db=None): path = '/rpc/cur_jump' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_dict = {'CUR': self.cursor_id} @@ -135,7 +134,7 @@ def jump_back(self, key=None, db=None): path = '/rpc/cur_jump_back' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_dict = {'CUR': self.cursor_id} @@ -330,7 +329,7 @@ def delete(self): class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer=None): + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): self.err = kt_error.KyotoTycoonError() self.pack_type = pack_type @@ -338,7 +337,8 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, pickle_protocol=2, custom_packer= raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') if pack_type == KT_PACKER_PICKLE: - self.pack = lambda data: pickle.dumps(data, pickle_protocol) + # Pickle protocol v2 is is used here instead of the default... + self.pack = lambda data: pickle.dumps(data, 2) self.unpack = lambda data: pickle.loads(data) elif pack_type == KT_PACKER_JSON: @@ -407,7 +407,7 @@ def echo(self): def get(self, key, db=None): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('GET', key) @@ -431,7 +431,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): path = '/rpc/set_bulk' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = ['atomic\t\n' if atomic else ''] @@ -458,7 +458,7 @@ def remove_bulk(self, keys, atomic, db): path = '/rpc/remove_bulk' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = ['atomic\t\n' if atomic else ''] @@ -488,7 +488,7 @@ def get_bulk(self, keys, atomic, db): path = '/rpc/get_bulk' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = ['atomic\t\n' if atomic else ''] @@ -521,7 +521,7 @@ def get_bulk(self, keys, atomic, db): def get_int(self, key, db=None): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('GET', key) @@ -537,7 +537,7 @@ def get_int(self, key, db=None): def vacuum(self, db): path = '/rpc/vacuum' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('GET', path) @@ -557,7 +557,7 @@ def match_prefix(self, prefix, max, db): path = '/rpc/match_prefix' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_dict = {'prefix': prefix.encode('utf-8')} @@ -595,7 +595,7 @@ def match_regex(self, regex, max, db): path = '/rpc/match_regex' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_dict = {'regex': regex.encode('utf-8')} @@ -629,7 +629,7 @@ def match_regex(self, regex, max, db): def set(self, key, value, expire, db): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -644,7 +644,7 @@ def set(self, key, value, expire, db): def add(self, key, value, expire, db): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -663,7 +663,7 @@ def cas(self, key, old_val, new_val, expire, db): path = '/rpc/cas' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_dict = {'key': key.encode('utf-8')} @@ -692,7 +692,7 @@ def cas(self, key, old_val, new_val, expire, db): def remove(self, key, db): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) self.conn.request('DELETE', key) @@ -708,7 +708,7 @@ def remove(self, key, db): def replace(self, key, value, expire, db): key = quote(key.encode('utf-8')) if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) key = '/%s/%s' % (db, key) value = self.pack(value) @@ -760,7 +760,7 @@ def append(self, key, value, expire, db): def increment(self, key, delta, expire, db): path = '/rpc/increment' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = 'key\t%s\nnum\t%d\n' % (key, delta) @@ -781,7 +781,7 @@ def increment_double(self, key, delta, expire, db): path = '/rpc/increment_double' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db request_body = 'key\t%s\nnum\t%f\n' % (key, delta) @@ -813,7 +813,7 @@ def report(self): def status(self, db=None): path = '/rpc/status' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('GET', path) @@ -833,7 +833,7 @@ def status(self, db=None): def clear(self, db=None): path = '/rpc/clear' if db: - db = db if isinstance(db, int) else quote(db.encode('utf-8')) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path += '?DB=' + db self.conn.request('GET', path) @@ -894,4 +894,4 @@ def _rest_put(self, operation, key, value, expire): res, body = self.getresponse() return res.status -# vim: set expandtab ts=4 sw=4 +# EOF - kt_http.py diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 1170d47..0557068 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2011, Toru Maesaka @@ -12,16 +11,25 @@ from . import kt_http from . import kt_binary +from .kt_common import KT_PACKER_PICKLE + KT_DEFAULT_HOST = '127.0.0.1' KT_DEFAULT_PORT = 1978 KT_DEFAULT_TIMEOUT = 30 class KyotoTycoon(object): - def __init__(self, binary=False, *args, **kwargs): - '''Initialize a "Binary Protocol" or "HTTP" KyotoTycoon object.''' + def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, custom_packer=None): + ''' + Initialize a "Binary Protocol" or "HTTP Protocol" KyotoTycoon object. + + Note: The default packer uses pickle protocol v2, which is the highest + version that's still compatible with both Python 2 and 3. If you + require a different version, specify a custom packer object. + + ''' protocol = kt_binary if binary else kt_http - self.core = protocol.ProtocolHandler(*args, **kwargs) + self.core = protocol.ProtocolHandler(pack_type, custom_packer) def error(self): '''Return the error state from the last operation.''' @@ -159,4 +167,4 @@ def play_script(self, name, kv_dict=None): return self.core.play_script(name, kv_dict) -# vim: set expandtab ts=4 sw=4 +# EOF - kyototycoon.py diff --git a/tests/kt_performance.py b/tests/kt_performance.py index f59baeb..fd710c8 100644 --- a/tests/kt_performance.py +++ b/tests/kt_performance.py @@ -37,7 +37,7 @@ from time import time from getopt import getopt, GetoptError -from kyototycoon import KyotoTycoon +from kyototycoon import KyotoTycoon, KT_PACKER_PICKLE, KT_PACKER_JSON, KT_PACKER_STRING NUM_ITERATIONS = 2000 @@ -88,9 +88,9 @@ def main(): is_unicode = isinstance("", type(unicode_literal)) for binary in (True, False): - for packer_type, packer_name in ((1, "KT_PACKER_PICKLE"), - (2, "KT_PACKER_JSON"), - (3, "KT_PACKER_STRING")): + for packer_type, packer_name in ((KT_PACKER_PICKLE, "KT_PACKER_PICKLE"), + (KT_PACKER_JSON, "KT_PACKER_JSON"), + (KT_PACKER_STRING, "KT_PACKER_STRING")): kt = KyotoTycoon(binary=binary, pack_type=packer_type) kt.open(server["host"], server["port"], timeout=2) @@ -131,4 +131,4 @@ def main(): main() -# vim: set expandtab ts=4 sw=4 +# EOF - kt_performance.py diff --git a/tests/t_all.py b/tests/t_all.py index e167da6..69bf85a 100644 --- a/tests/t_all.py +++ b/tests/t_all.py @@ -5,11 +5,11 @@ # Redistribution and use of this source code is licensed under # the BSD license. See COPYING file for license description. # +# Kyoto Tycoon should be started like this: +# $ ktserver -ls -scr t_script.lua '%' '%' +# # USAGE: # $ python t_all.py -# $ python t_all.py ExpireTestCase -# $ python t_all.py MultiTestCase -# $ python t_all.py ScriptTestCase import os import re @@ -25,20 +25,10 @@ def _run_all_tests(): for filename in os.listdir(test_path): match = _TEST_MODULE_PATTERN.search(filename) if match: - case = match.group(1) - if case not in ('t_expire', 't_multi', 't_script'): - module_names.append(case) + module_names.append(match.group(1)) return loader.loadTestsFromNames(module_names) -def ExpireTestCase(): - loader = unittest.TestLoader() - return loader.loadTestsFromName('t_expire') - -def MultiTestCase(): - loader = unittest.TestLoader() - return loader.loadTestsFromName('t_multi') - def ScriptTestCase(): loader = unittest.TestLoader() return loader.loadTestsFromName('t_script') diff --git a/tests/t_expire.py b/tests/t_expire.py index 24e3d37..95f3c41 100644 --- a/tests/t_expire.py +++ b/tests/t_expire.py @@ -27,14 +27,14 @@ def test_set_expire(self): self.assertTrue(self.kt_http_handle.set('key1', 'value', 2)) self.assertEqual(self.kt_http_handle.get('key1'), 'value') self.assertEqual(self.kt_http_handle.count(), 1) - time.sleep(3) + time.sleep(4) self.assertEqual(self.kt_http_handle.count(), 0) # Set record to be expired in 2 seconds. self.assertTrue(self.kt_bin_handle.set('key2', 'value', 2)) self.assertEqual(self.kt_bin_handle.get('key2'), 'value') self.assertEqual(self.kt_http_handle.count(), 1) - time.sleep(3) + time.sleep(4) self.assertEqual(self.kt_http_handle.count(), 0) def test_add_expire(self): @@ -42,7 +42,7 @@ def test_add_expire(self): self.assertTrue(self.kt_http_handle.add('hello', 'world', 2)) self.assertEqual(self.kt_http_handle.get('hello'), 'world') - time.sleep(3) + time.sleep(4) self.assertEqual(self.kt_http_handle.get('hello'), None) if __name__ == '__main__': diff --git a/tests/t_multi.py b/tests/t_multi.py index b18df75..6ae6eb1 100644 --- a/tests/t_multi.py +++ b/tests/t_multi.py @@ -6,177 +6,241 @@ # the BSD license. See COPYING file for license description. # # Kyoto Tycoon should be started like this: -# $ ktserver one.kch two.kch three.kch +# $ ktserver one.kch two.kch import config import time import unittest from kyototycoon import KyotoTycoon -DB_1 = 'one.kch' -DB_2 = 'two.kch' -DB_INVALID = 'invalid.kch' +DB_1 = 0 +DB_2 = 1 +DB_INVALID = 3 class UnitTest(unittest.TestCase): def setUp(self): - self.kt_handle = KyotoTycoon() - self.kt_handle.open() + self.kt_handle_http = KyotoTycoon(binary=False) + self.kt_handle_http.open() + + self.kt_handle_bin = KyotoTycoon(binary=True) + self.kt_handle_bin.open() + self.LARGE_KEY_LEN = 8000 def clear_all(self): - self.assertTrue(self.kt_handle.clear(db=DB_1)) - self.assertTrue(self.kt_handle.clear(db=DB_2)) + self.assertTrue(self.kt_handle_http.clear(db=DB_1)) + self.assertTrue(self.kt_handle_http.clear(db=DB_2)) return True def test_status(self): - status = self.kt_handle.status(DB_1) + status = self.kt_handle_http.status(DB_1) assert status is not None - status = self.kt_handle.status(DB_2) + status = self.kt_handle_http.status(DB_2) assert status is not None - status = self.kt_handle.status(DB_INVALID) + status = self.kt_handle_http.status(DB_INVALID) assert status is None - status = self.kt_handle.status('non_existent') + status = self.kt_handle_http.status('non_existent') assert status is None def test_set_get(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.set('ice', 'cream', db=DB_2)) - self.assertFalse(self.kt_handle.set('palo', 'alto', db=DB_INVALID)) + self.assertTrue(self.kt_handle_http.set('ice', 'cream', db=DB_2)) + self.assertFalse(self.kt_handle_http.set('palo', 'alto', db=DB_INVALID)) - self.assertEqual(self.kt_handle.get('ice'), None) - self.assertEqual(self.kt_handle.get('ice', db=DB_1), None) - self.assertFalse(self.kt_handle.get('ice', db=DB_INVALID)) + self.assertEqual(self.kt_handle_http.get('ice'), None) + self.assertEqual(self.kt_handle_http.get('ice', db=DB_1), None) + self.assertFalse(self.kt_handle_http.get('ice', db=DB_INVALID)) - self.assertEqual(self.kt_handle.get('ice', db='two.kch'), 'cream') - self.assertEqual(self.kt_handle.count(db=DB_1), 0) - self.assertEqual(self.kt_handle.count(db=DB_2), 1) + self.assertEqual(self.kt_handle_http.get('ice', db=DB_2), 'cream') + self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) + self.assertEqual(self.kt_handle_http.count(db=DB_2), 1) - self.assertTrue(self.kt_handle.set('frozen', 'yoghurt', db=DB_1)) - self.assertEqual(self.kt_handle.count(db=DB_1), 1) + self.assertTrue(self.kt_handle_http.set('frozen', 'yoghurt', db=DB_1)) + self.assertEqual(self.kt_handle_http.count(db=DB_1), 1) - self.assertEqual(self.kt_handle.get('frozen'), 'yoghurt') - self.assertEqual(self.kt_handle.get('frozen', db=DB_1), 'yoghurt') - self.assertEqual(self.kt_handle.get('frozen', db=DB_2), None) - self.assertFalse(self.kt_handle.get('frozen', db=DB_INVALID), None) + self.assertEqual(self.kt_handle_http.get('frozen'), 'yoghurt') + self.assertEqual(self.kt_handle_http.get('frozen', db=DB_1), 'yoghurt') + self.assertEqual(self.kt_handle_http.get('frozen', db=DB_2), None) + self.assertFalse(self.kt_handle_http.get('frozen', db=DB_INVALID), None) - self.assertTrue(self.kt_handle.clear(db=DB_1)) - self.assertEqual(self.kt_handle.count(db=DB_1), 0) - self.assertEqual(self.kt_handle.count(db=DB_2), 1) + self.assertTrue(self.kt_handle_http.clear(db=DB_1)) + self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) + self.assertEqual(self.kt_handle_http.count(db=DB_2), 1) def test_get_multi(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.set('a', 'xxxx', db=DB_1)) - self.assertTrue(self.kt_handle.set('b', 'yyyy', db=DB_1)) - self.assertTrue(self.kt_handle.set('c', 'zzzz', db=DB_1)) - self.assertTrue(self.kt_handle.set('a1', 'xxxx', db=DB_2)) - self.assertTrue(self.kt_handle.set('b1', 'yyyy', db=DB_2)) - self.assertTrue(self.kt_handle.set('c1', 'zzzz', db=DB_2)) + self.assertTrue(self.kt_handle_http.set('a', 'xxxx', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('b', 'yyyy', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('c', 'zzzz', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('a1', 'xxxx', db=DB_2)) + self.assertTrue(self.kt_handle_http.set('b1', 'yyyy', db=DB_2)) + self.assertTrue(self.kt_handle_http.set('c1', 'zzzz', db=DB_2)) - d = self.kt_handle.get_bulk(['a', 'b', 'c'], db=DB_1) + d = self.kt_handle_http.get_bulk(['a', 'b', 'c'], db=DB_1) self.assertEqual(len(d), 3) self.assertEqual(d['a'], 'xxxx') self.assertEqual(d['b'], 'yyyy') self.assertEqual(d['c'], 'zzzz') - d = self.kt_handle.get_bulk(['a', 'b', 'c'], db=DB_2) + d = self.kt_handle_http.get_bulk(['a', 'b', 'c'], db=DB_2) self.assertEqual(len(d), 0) - d = self.kt_handle.get_bulk(['a1', 'b1', 'c1'], db=DB_2) + d = self.kt_handle_http.get_bulk(['a1', 'b1', 'c1'], db=DB_2) self.assertEqual(len(d), 3) self.assertEqual(d['a1'], 'xxxx') self.assertEqual(d['b1'], 'yyyy') self.assertEqual(d['c1'], 'zzzz') - d = self.kt_handle.get_bulk(['a1', 'b1', 'c1'], db=DB_1) + d = self.kt_handle_http.get_bulk(['a1', 'b1', 'c1'], db=DB_1) self.assertEqual(len(d), 0) def test_add(self): self.assertTrue(self.clear_all()) # Should not conflict due to different databases. - self.assertTrue(self.kt_handle.add('key1', 'val1', db=DB_1)) - self.assertTrue(self.kt_handle.add('key1', 'val1', db=DB_2)) + self.assertTrue(self.kt_handle_http.add('key1', 'val1', db=DB_1)) + self.assertTrue(self.kt_handle_http.add('key1', 'val1', db=DB_2)) # Now they should. - self.assertFalse(self.kt_handle.add('key1', 'val1', db=DB_1)) - self.assertFalse(self.kt_handle.add('key1', 'val1', db=DB_2)) - self.assertFalse(self.kt_handle.add('key1', 'val1', db=DB_INVALID)) + self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_1)) + self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_2)) + self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_INVALID)) def test_replace(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.add('key1', 'val1', db=DB_1)) - self.assertFalse(self.kt_handle.replace('key1', 'val2', db=DB_2)) - self.assertTrue(self.kt_handle.replace('key1', 'val2', db=DB_1)) - self.assertFalse(self.kt_handle.replace('key1', 'val2', db=DB_INVALID)) + self.assertTrue(self.kt_handle_http.add('key1', 'val1', db=DB_1)) + self.assertFalse(self.kt_handle_http.replace('key1', 'val2', db=DB_2)) + self.assertTrue(self.kt_handle_http.replace('key1', 'val2', db=DB_1)) + self.assertFalse(self.kt_handle_http.replace('key1', 'val2', db=DB_INVALID)) - self.assertTrue(self.kt_handle.add('key2', 'aaa')) - self.assertTrue(self.kt_handle.replace('key2', 'bbb')) - self.assertTrue(self.kt_handle.replace('key1', 'zzz')) - self.assertEqual(self.kt_handle.get('key2'), 'bbb') - self.assertEqual(self.kt_handle.get('key1'), 'zzz') + self.assertTrue(self.kt_handle_http.add('key2', 'aaa')) + self.assertTrue(self.kt_handle_http.replace('key2', 'bbb')) + self.assertTrue(self.kt_handle_http.replace('key1', 'zzz')) + self.assertEqual(self.kt_handle_http.get('key2'), 'bbb') + self.assertEqual(self.kt_handle_http.get('key1'), 'zzz') def test_cas(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.set('key', 'xxx')) - self.assertEqual(self.kt_handle.get('key', db=DB_2), None) - self.assertFalse(self.kt_handle.cas('key', old_val='xxx', new_val='yyy', db=DB_2)) - self.assertEqual(self.kt_handle.get('key', db=DB_1), 'xxx') - self.assertTrue(self.kt_handle.cas('key', old_val='xxx', new_val='yyy', db=DB_1)) - self.assertTrue(self.kt_handle.cas('key', new_val='xxx', db=DB_2)) + self.assertTrue(self.kt_handle_http.set('key', 'xxx')) + self.assertEqual(self.kt_handle_http.get('key', db=DB_2), None) + self.assertFalse(self.kt_handle_http.cas('key', old_val='xxx', new_val='yyy', db=DB_2)) + self.assertEqual(self.kt_handle_http.get('key', db=DB_1), 'xxx') + self.assertTrue(self.kt_handle_http.cas('key', old_val='xxx', new_val='yyy', db=DB_1)) + self.assertTrue(self.kt_handle_http.cas('key', new_val='xxx', db=DB_2)) def test_remove(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.add('key', 'value', db=DB_1)) - self.assertTrue(self.kt_handle.add('key', 'value', db=DB_2)) + self.assertTrue(self.kt_handle_http.add('key', 'value', db=DB_1)) + self.assertTrue(self.kt_handle_http.add('key', 'value', db=DB_2)) - self.assertTrue(self.kt_handle.remove('key', db=DB_1)) - self.assertEqual(self.kt_handle.get('key', db=DB_2), 'value') - assert self.kt_handle.get('key', db=DB_1) is None + self.assertTrue(self.kt_handle_http.remove('key', db=DB_1)) + self.assertEqual(self.kt_handle_http.get('key', db=DB_2), 'value') + assert self.kt_handle_http.get('key', db=DB_1) is None def test_vacuum(self): - self.assertTrue(self.kt_handle.vacuum()) - self.assertTrue(self.kt_handle.vacuum(db=DB_1)) - self.assertTrue(self.kt_handle.vacuum(db=DB_2)) - self.assertFalse(self.kt_handle.vacuum(db=DB_INVALID)) + self.assertTrue(self.kt_handle_http.vacuum()) + self.assertTrue(self.kt_handle_http.vacuum(db=DB_1)) + self.assertTrue(self.kt_handle_http.vacuum(db=DB_2)) + self.assertFalse(self.kt_handle_http.vacuum(db=DB_INVALID)) def test_append(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.set('key', 'xxx', db=DB_1)) - self.assertTrue(self.kt_handle.set('key', 'xxx', db=DB_2)) - self.assertTrue(self.kt_handle.append('key', 'xxx', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('key', 'xxx', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('key', 'xxx', db=DB_2)) + self.assertTrue(self.kt_handle_http.append('key', 'xxx', db=DB_1)) - self.assertEqual(self.kt_handle.get('key', db=DB_1), 'xxxxxx') - self.assertEqual(self.kt_handle.get('key', db=DB_2), 'xxx') + self.assertEqual(self.kt_handle_http.get('key', db=DB_1), 'xxxxxx') + self.assertEqual(self.kt_handle_http.get('key', db=DB_2), 'xxx') def test_increment(self): self.assertTrue(self.clear_all()) - self.assertEqual(self.kt_handle.increment('key', 0, db=DB_1), 0) - self.assertEqual(self.kt_handle.increment('key', 0, db=DB_2), 0) + self.assertEqual(self.kt_handle_http.increment('key', 0, db=DB_1), 0) + self.assertEqual(self.kt_handle_http.increment('key', 0, db=DB_2), 0) - self.assertEqual(self.kt_handle.increment('key', 100, db=DB_1), 100) - self.assertEqual(self.kt_handle.increment('key', 200, db=DB_2), 200) - self.assertEqual(self.kt_handle.increment('key', 100, db=DB_1), 200) - self.assertEqual(self.kt_handle.increment('key', 200, db=DB_2), 400) - self.assertEqual(self.kt_handle.get_int('key', db=DB_1), 200) - self.assertEqual(self.kt_handle.get_int('key', db=DB_2), 400) + self.assertEqual(self.kt_handle_http.increment('key', 100, db=DB_1), 100) + self.assertEqual(self.kt_handle_http.increment('key', 200, db=DB_2), 200) + self.assertEqual(self.kt_handle_http.increment('key', 100, db=DB_1), 200) + self.assertEqual(self.kt_handle_http.increment('key', 200, db=DB_2), 400) + self.assertEqual(self.kt_handle_http.get_int('key', db=DB_1), 200) + self.assertEqual(self.kt_handle_http.get_int('key', db=DB_2), 400) def test_match_prefix(self): self.assertTrue(self.clear_all()) - self.assertTrue(self.kt_handle.set('abcdef', 'val', db=DB_1)) - self.assertTrue(self.kt_handle.set('fedcba', 'val', db=DB_2)) + self.assertTrue(self.kt_handle_http.set('abcdef', 'val', db=DB_1)) + self.assertTrue(self.kt_handle_http.set('fedcba', 'val', db=DB_2)) - list = self.kt_handle.match_prefix('abc', db=DB_1) + list = self.kt_handle_http.match_prefix('abc', db=DB_1) self.assertEqual(len(list), 1) self.assertEqual(list[0], 'abcdef') - list = self.kt_handle.match_prefix('abc', db=DB_2) + list = self.kt_handle_http.match_prefix('abc', db=DB_2) self.assertEqual(len(list), 0) - list = self.kt_handle.match_prefix('fed', db=DB_1) + list = self.kt_handle_http.match_prefix('fed', db=DB_1) self.assertEqual(len(list), 0) - list = self.kt_handle.match_prefix('fed', db=DB_2) + list = self.kt_handle_http.match_prefix('fed', db=DB_2) self.assertEqual(len(list), 1) self.assertEqual(list[0], 'fedcba') + def test_set_get_bin(self): + self.assertTrue(self.clear_all()) + self.assertTrue(self.kt_handle_bin.set('ice', 'cream', db=DB_2)) + self.assertFalse(self.kt_handle_bin.set('palo', 'alto', db=DB_INVALID)) + + self.assertEqual(self.kt_handle_bin.get('ice'), None) + self.assertEqual(self.kt_handle_bin.get('ice', db=DB_1), None) + self.assertFalse(self.kt_handle_bin.get('ice', db=DB_INVALID)) + + self.assertEqual(self.kt_handle_bin.get('ice', db=DB_2), 'cream') + self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) + self.assertEqual(self.kt_handle_http.count(db=DB_2), 1) + + self.assertTrue(self.kt_handle_bin.set('frozen', 'yoghurt', db=DB_1)) + self.assertEqual(self.kt_handle_http.count(db=DB_1), 1) + + self.assertEqual(self.kt_handle_bin.get('frozen'), 'yoghurt') + self.assertEqual(self.kt_handle_bin.get('frozen', db=DB_1), 'yoghurt') + self.assertEqual(self.kt_handle_bin.get('frozen', db=DB_2), None) + self.assertFalse(self.kt_handle_bin.get('frozen', db=DB_INVALID), None) + + self.assertTrue(self.kt_handle_http.clear(db=DB_1)) + self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) + self.assertEqual(self.kt_handle_http.count(db=DB_2), 1) + + def test_get_multi_bin(self): + self.assertTrue(self.clear_all()) + + self.assertTrue(self.kt_handle_bin.set('a', 'xxxx', db=DB_1)) + self.assertTrue(self.kt_handle_bin.set('b', 'yyyy', db=DB_1)) + self.assertTrue(self.kt_handle_bin.set('c', 'zzzz', db=DB_1)) + self.assertTrue(self.kt_handle_bin.set('a1', 'xxxx', db=DB_2)) + self.assertTrue(self.kt_handle_bin.set('b1', 'yyyy', db=DB_2)) + self.assertTrue(self.kt_handle_bin.set('c1', 'zzzz', db=DB_2)) + + d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_1) + self.assertEqual(len(d), 3) + self.assertEqual(d['a'], 'xxxx') + self.assertEqual(d['b'], 'yyyy') + self.assertEqual(d['c'], 'zzzz') + d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_2) + self.assertEqual(len(d), 0) + + d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_2) + self.assertEqual(len(d), 3) + self.assertEqual(d['a1'], 'xxxx') + self.assertEqual(d['b1'], 'yyyy') + self.assertEqual(d['c1'], 'zzzz') + d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_1) + self.assertEqual(len(d), 0) + + def test_remove_bin(self): + self.assertTrue(self.clear_all()) + self.assertTrue(self.kt_handle_bin.set('key', 'value', db=DB_1)) + self.assertTrue(self.kt_handle_bin.set('key', 'value', db=DB_2)) + + self.assertTrue(self.kt_handle_bin.remove('key', db=DB_1)) + self.assertEqual(self.kt_handle_bin.get('key', db=DB_2), 'value') + assert self.kt_handle_bin.get('key', db=DB_1) is None + if __name__ == '__main__': unittest.main() diff --git a/tests/t_script.lua b/tests/t_script.lua new file mode 100644 index 0000000..0b1c22c --- /dev/null +++ b/tests/t_script.lua @@ -0,0 +1,51 @@ +-- This is a subset of "example/ktscrex.lua" from the Kyoto Tycoon 0.9.56 source distribution. + +kt = __kyototycoon__ +db = kt.db + +-- log the start-up message +if kt.thid == 0 then + kt.log("system", "loaded Lua script for 'play_script' unit tests") +end + +-- echo back the input data as the output data +function echo(inmap, outmap) + for key, value in pairs(inmap) do + outmap[key] = value + end + return kt.RVSUCCESS +end + +-- store a record +function set(inmap, outmap) + local key = inmap.key + local value = inmap.value + if not key or not value then + return kt.RVEINVALID + end + local xt = inmap.xt + if not db:set(key, value, xt) then + return kt.RVEINTERNAL + end + return kt.RVSUCCESS +end + +-- retrieve the value of a record +function get(inmap, outmap) + local key = inmap.key + if not key then + return kt.RVEINVALID + end + local value, xt = db:get(key) + if value then + outmap.value = value + outmap.xt = xt + else + local err = db:error() + if err:code() == kt.Error.NOREC then + return kt.RVELOGIC + end + return kt.RVEINTERNAL + end + return kt.RVSUCCESS +end diff --git a/tests/t_script.py b/tests/t_script.py index 4cc5f81..9498a2e 100644 --- a/tests/t_script.py +++ b/tests/t_script.py @@ -4,6 +4,9 @@ # # Redistribution and use of this source code is licensed under # the BSD license. See COPYING file for license description. +# +# Kyoto Tycoon should be started like this: +# $ ktserver -scr example/ktscrex.lua '%' import config import unittest From 93c8e813c8dcd6c1375cb526ae2e7ccadb31eea6 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 9 Feb 2014 15:09:31 +0000 Subject: [PATCH 44/62] Update README. Bump version. --- README | 47 ++++++++++++++++++++++++++++++++--------------- setup.py | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/README b/README index a14e78c..d0e8d36 100644 --- a/README +++ b/README @@ -1,7 +1,8 @@ ABOUT ----- -Python client library for Kyoto Tycoon. For more information on -Kyoto Tycoon please refer to the official project website: +This is a native Python client library for Kyoto Tycoon. +For more information on Kyoto Tycoon please refer to the +official project website: http://fallabs.com/kyototycoon/ @@ -10,13 +11,14 @@ provided by Kyoto Tycoon's original author(s): http://fallabs.com/kyototycoon/kyototycoon.idl -There is currently no documentation for this software but in -the meantime please see the file "kyototycoon.py" for the +There is currently no documentation for this software but, in +the meantime, please see the file "kyototycoon.py" for the interface. -This fork includes cursor support and significant performance -improvements over the original library. It also supports unicode -keys and works with both Python 2.x and 3.x. +This library includes cursor support and significant performance +improvements over the (now unmaintained) original library by +Toru Maesaka. It also supports unicode keys and works with both +Python 2 and 3. The more efficient binary protocol is also supported along with the HTTP protocol. It provides a performance improvement of up @@ -56,24 +58,39 @@ the called script returns and must do the marshalling itself. INSTALLATION ------------ -Using pip:: +You can install the latest version of this library from source:: + + python setup.py build + python setup.py install + +A version of this library is available from PyPI, although it may +not always be the latest and greatest version. You can find it at: + + https://pypi.python.org/pypi/python-kyototycoon/ + +You can install packages directly from PyPI using ``pip``:: pip install python-kyototycoon -Or, from source:: +This library is still not at version 1.0, which means the API and +behavior are not guaranteed to remain consistent between versions. +If you require a specific version consider using versioning with +``pip``. For example:: - python setup.py build - sudo python setup.py install + pip install python-kyototycoon==0.4.6 + +This is ideal for use with any automatic scripts you may be using +to build/deploy your application. AUTHORS ------- * Toru Maesaka * Stephen Hamer + * Carlos Rodrigues -Binary protocol support was added by Carlos Rodrigues based -on Ulrich Mierendorff's code with only minimal changes to make -it fit this library. You can find the original library at the -following URL: +Binary protocol support was added based on Ulrich Mierendorff's +code with only minimal changes to make it fit this library. +You can find the original library at the following URL: http://www.ulrichmierendorff.com/software/kyoto_tycoon/python_library.html diff --git a/setup.py b/setup.py index c3337f5..ad8536a 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.5', + version='0.5.6', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], From c0f6c308314e352988a93bce124d3da65292d721 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 11 Feb 2014 19:27:18 +0000 Subject: [PATCH 45/62] Add 'ok()' method to KyotoTycoonError make checking for errors cleaner. --- kyototycoon/kt_error.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index dacbae5..c144ff2 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -50,6 +50,9 @@ def set_error(self, code): self.error_name = self.ErrorNameDict[code] self.error_message = self.ErrorMessageDict[code] + def ok(self): + return self.error_code == self.SUCCESS + def code(self): return self.error_code From 22303c372bbbc7890949c4bc8101abf007a80a66 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 12 Feb 2014 23:15:41 +0000 Subject: [PATCH 46/62] Optionally raise exceptions in addition to silently setting an error code. --- kyototycoon/__init__.py | 2 ++ kyototycoon/kt_binary.py | 10 ++++----- kyototycoon/kt_error.py | 45 +++++++++++++++++++++++++++++++------- kyototycoon/kt_http.py | 24 ++++++++++---------- kyototycoon/kyototycoon.py | 9 ++++++-- setup.py | 2 +- 6 files changed, 64 insertions(+), 28 deletions(-) diff --git a/kyototycoon/__init__.py b/kyototycoon/__init__.py index 58a3f1f..cf7eb81 100644 --- a/kyototycoon/__init__.py +++ b/kyototycoon/__init__.py @@ -11,4 +11,6 @@ KT_PACKER_STRING, \ KT_PACKER_BYTES +from .kt_error import KyotoTycoonException + # EOF - __init__.py diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 9d0c68d..cdebd1f 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -39,8 +39,8 @@ DEFAULT_EXPIRE = 0x7fffffffffffffff class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): - self.err = kt_error.KyotoTycoonError() + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): + self.err = kt_error.KyotoTycoonError(exceptions) self.socket = None if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: @@ -94,7 +94,7 @@ def get(self, key, db=None): def set_bulk(self, kv_dict, expire, atomic, db): if isinstance(kv_dict, dict) and len(kv_dict) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no key:value pairs specified') return 0 if expire is None: @@ -123,7 +123,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): def remove_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no keys specified') return 0 if db is None: @@ -152,7 +152,7 @@ def remove_bulk(self, keys, atomic, db): def get_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no keys specified') return {} if db is None: diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index c144ff2..06a3639 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -5,6 +5,9 @@ # Redistribution and use of this source code is licensed under # the BSD license. See COPYING file for license description. +class KyotoTycoonException(Exception): + pass + class KyotoTycoonError(object): SUCCESS = 0 NOIMPL = 1 @@ -37,29 +40,55 @@ class KyotoTycoonError(object): EMISC: "Miscellaneous Error", } - def __init__(self): + def __init__(self, exceptions=False): + ''' + Initialize the last database operation error object. + + The "exceptions" parameter controls whether an exception is raised when + the library sets the error state to something that's an actual error. + + ''' + + self.exceptions = exceptions self.set_success() - def set_success(self): - self.error_code = self.SUCCESS - self.error_name = self.ErrorNameDict[self.SUCCESS] - self.error_message = self.ErrorMessageDict[self.SUCCESS] + def set_error(self, code, detail_message=None): + '''Set the error state for the last database operation.''' - def set_error(self, code): self.error_code = code self.error_name = self.ErrorNameDict[code] self.error_message = self.ErrorMessageDict[code] + self.error_detail = detail_message + + if self.exceptions and not self.ok(): + raise KyotoTycoonException(self.message()) + + def set_success(self): + '''Flag the last database operation as having been successful.''' + + self.set_error(self.SUCCESS) def ok(self): - return self.error_code == self.SUCCESS + '''Return whether the last database operation resulted in an error.''' + + return self.error_code in (self.SUCCESS, self.NOTFOUND) def code(self): + '''Return the error code for the last database operation.''' + return self.error_code def name(self): + '''Return the error name for the last database operation.''' + return self.error_name def message(self): - return self.error_message + '''Return the error description for the last database operation.''' + + if self.error_detail: + return '%s (%s)' % (self.error_message, self.error_detail) + else: + return self.error_message # EOF - kt_error.py diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index cef6649..669b01c 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -329,8 +329,8 @@ def delete(self): class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): - self.err = kt_error.KyotoTycoonError() + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): + self.err = kt_error.KyotoTycoonError(exceptions) self.pack_type = pack_type if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: @@ -426,7 +426,7 @@ def get(self, key, db=None): def set_bulk(self, kv_dict, expire, atomic, db): if isinstance(kv_dict, dict) and len(kv_dict) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no key:value pairs specified') return 0 path = '/rpc/set_bulk' @@ -453,7 +453,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): def remove_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no keys specified') return 0 path = '/rpc/remove_bulk' @@ -483,7 +483,7 @@ def remove_bulk(self, keys, atomic, db): def get_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no keys specified') return {} path = '/rpc/get_bulk' @@ -552,7 +552,7 @@ def vacuum(self, db): def match_prefix(self, prefix, max, db): if prefix is None: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no key prefix specified') return None path = '/rpc/match_prefix' @@ -590,7 +590,7 @@ def match_prefix(self, prefix, max, db): def match_regex(self, regex, max, db): if regex is None: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no regular expression specified') return None path = '/rpc/match_regex' @@ -658,7 +658,7 @@ def add(self, key, value, expire, db): def cas(self, key, old_val, new_val, expire, db): if old_val is None and new_val is None: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'old value and/or new value must be specified') return False path = '/rpc/cas' @@ -739,7 +739,7 @@ def append(self, key, value, expire, db): if (not isinstance(data, bytes_type) and not isinstance(data, unicode_type)): - self.err.set_error(self.err.EMISC) + self.err.set_error(self.err.EMISC, 'stored value is not a string or bytes type') return False if type(data) != type(value): @@ -751,7 +751,7 @@ def append(self, key, value, expire, db): data += value if self.set(key, data, expire, db) is not True: - self.err.set_error(self.err.EMISC) + self.err.set_error(self.err.EMISC, 'error while storing modified value') return False self.err.set_success() @@ -776,7 +776,7 @@ def increment(self, key, delta, expire, db): def increment_double(self, key, delta, expire, db): if key is None: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.LOGIC, 'no key specified') return False path = '/rpc/increment_double' @@ -872,7 +872,7 @@ def play_script(self, name, kv_dict=None): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.LOGIC) + self.err.set_error(self.err.MISC) return None rv = {} diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 0557068..2e3c533 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -18,10 +18,15 @@ KT_DEFAULT_TIMEOUT = 30 class KyotoTycoon(object): - def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, custom_packer=None): + def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, + custom_packer=None, exceptions=False): ''' Initialize a "Binary Protocol" or "HTTP Protocol" KyotoTycoon object. + The library doesn't raise exceptions for server errors (soft errors) by + default (for historical reasons). This behavior can be changed by using + the "exceptions" parameter. + Note: The default packer uses pickle protocol v2, which is the highest version that's still compatible with both Python 2 and 3. If you require a different version, specify a custom packer object. @@ -29,7 +34,7 @@ def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, custom_packer=None) ''' protocol = kt_binary if binary else kt_http - self.core = protocol.ProtocolHandler(pack_type, custom_packer) + self.core = protocol.ProtocolHandler(pack_type, custom_packer, exceptions) def error(self): '''Return the error state from the last operation.''' diff --git a/setup.py b/setup.py index ad8536a..b76aefe 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.6', + version='0.5.7', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], From 3892f71d26d43efcaed0575aded38297778484a5 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Thu, 13 Feb 2014 17:35:12 +0000 Subject: [PATCH 47/62] Raise an exception if 'atomic=True' is used with the binary protocol. --- README | 11 +++++++---- kyototycoon/kt_binary.py | 25 +++++++++++++++++-------- kyototycoon/kt_http.py | 12 ++++++------ tests/t_multi.py | 8 ++++---- tests/t_simple_binary.py | 16 ++++++++-------- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/README b/README index d0e8d36..8837868 100644 --- a/README +++ b/README @@ -29,10 +29,13 @@ to 6x, but only the following operations are available: * ``remove()`` and ``remove_bulk()`` * ``play_script()`` -Other operations will throw a ``NotImplementedError`` exception -but, as a last resort, it's always possible to have two -KyotoTycoon objects open to the same server, one using HTTP and -the other using the binary protocol. +Atomic operations aren't supported with the binary protocol, +the use of "atomic=False" is mandatory when using it. Operations +besides these will raise a ``NotImplementedError`` exception. + +It's possible to have two KyotoTycoon objects open to the same +server in the same application, one using HTTP and the other +using the binary protocol, if necessary. The library does automatic packing and unpacking (marshalling) of values coming from/to the database. The following data diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index cdebd1f..15dadfd 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -12,7 +12,7 @@ import socket import struct -from . import kt_error +from .kt_error import KyotoTycoonError, KyotoTycoonException from .kt_common import KT_PACKER_CUSTOM, \ KT_PACKER_PICKLE, \ @@ -40,11 +40,11 @@ class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): - self.err = kt_error.KyotoTycoonError(exceptions) + self.err = KyotoTycoonError(exceptions) self.socket = None if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: - raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') + raise KyotoTycoonException('custom packer object supported for "KT_PACKER_CUSTOM" only') if pack_type == KT_PACKER_PICKLE: # Pickle protocol v2 is is used here instead of the default... @@ -65,13 +65,13 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=Fa elif pack_type == KT_PACKER_CUSTOM: if custom_packer is None: - raise Exception('"KT_PACKER_CUSTOM" requires a packer object') + raise KyotoTycoonException('"KT_PACKER_CUSTOM" requires a packer object') self.pack = custom_packer.pack self.unpack = custom_packer.unpack else: - raise Exception('unsupported pack type specified') + raise KyotoTycoonException('unsupported pack type specified') def error(self): return self.err @@ -89,10 +89,13 @@ def close(self): return True def get(self, key, db=None): - values = self.get_bulk([key], True, db) + values = self.get_bulk([key], False, db) return values[key] if values else None def set_bulk(self, kv_dict, expire, atomic, db): + if atomic: + raise KyotoTycoonException('atomic supported under the HTTP procotol only') + if isinstance(kv_dict, dict) and len(kv_dict) < 1: self.err.set_error(self.err.LOGIC, 'no key:value pairs specified') return 0 @@ -122,6 +125,9 @@ def set_bulk(self, kv_dict, expire, atomic, db): return num_items def remove_bulk(self, keys, atomic, db): + if atomic: + raise KyotoTycoonException('atomic supported under the HTTP procotol only') + if len(keys) < 1: self.err.set_error(self.err.LOGIC, 'no keys specified') return 0 @@ -151,6 +157,9 @@ def remove_bulk(self, keys, atomic, db): return num_items def get_bulk(self, keys, atomic, db): + if atomic: + raise KyotoTycoonException('atomic supported under the HTTP procotol only') + if len(keys) < 1: self.err.set_error(self.err.LOGIC, 'no keys specified') return {} @@ -199,7 +208,7 @@ def match_regex(self, regex, max, db): raise NotImplementedError('supported under the HTTP procotol only') def set(self, key, value, expire, db): - numitems = self.set_bulk({key: value}, expire, True, db) + numitems = self.set_bulk({key: value}, expire, False, db) return numitems > 0 def add(self, key, value, expire, db): @@ -209,7 +218,7 @@ def cas(self, key, old_val, new_val, expire, db): raise NotImplementedError('supported under the HTTP procotol only') def remove(self, key, db): - numitems = self.remove_bulk([key], True, db) + numitems = self.remove_bulk([key], False, db) return numitems > 0 def replace(self, key, value, expire, db): diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 669b01c..4d18669 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -10,7 +10,7 @@ import time import sys -from . import kt_error +from .kt_error import KyotoTycoonError, KyotoTycoonException from .kt_common import KT_PACKER_CUSTOM, \ KT_PACKER_PICKLE, \ @@ -95,7 +95,7 @@ def __init__(self, protocol_handler): self.cursor_id = Cursor.cursor_id_counter Cursor.cursor_id_counter += 1 - self.err = kt_error.KyotoTycoonError() + self.err = KyotoTycoonError() self.pack = self.protocol_handler.pack self.unpack = self.protocol_handler.unpack @@ -330,11 +330,11 @@ def delete(self): class ProtocolHandler(object): def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): - self.err = kt_error.KyotoTycoonError(exceptions) + self.err = KyotoTycoonError(exceptions) self.pack_type = pack_type if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: - raise Exception('custom packer object supported for "KT_PACKER_CUSTOM" only') + raise KyotoTycoonException('custom packer object supported for "KT_PACKER_CUSTOM" only') if pack_type == KT_PACKER_PICKLE: # Pickle protocol v2 is is used here instead of the default... @@ -355,13 +355,13 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=Fa elif pack_type == KT_PACKER_CUSTOM: if custom_packer is None: - raise Exception('"KT_PACKER_CUSTOM" requires a packer object') + raise KyotoTycoonException('"KT_PACKER_CUSTOM" requires a packer object') self.pack = custom_packer.pack self.unpack = custom_packer.unpack else: - raise Exception('unsupported pack type specified') + raise KyotoTycoonException('unsupported pack type specified') def error(self): return self.err diff --git a/tests/t_multi.py b/tests/t_multi.py index 6ae6eb1..17dad88 100644 --- a/tests/t_multi.py +++ b/tests/t_multi.py @@ -217,20 +217,20 @@ def test_get_multi_bin(self): self.assertTrue(self.kt_handle_bin.set('b1', 'yyyy', db=DB_2)) self.assertTrue(self.kt_handle_bin.set('c1', 'zzzz', db=DB_2)) - d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_1) + d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_1, atomic=False) self.assertEqual(len(d), 3) self.assertEqual(d['a'], 'xxxx') self.assertEqual(d['b'], 'yyyy') self.assertEqual(d['c'], 'zzzz') - d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_2) + d = self.kt_handle_bin.get_bulk(['a', 'b', 'c'], db=DB_2, atomic=False) self.assertEqual(len(d), 0) - d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_2) + d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_2, atomic=False) self.assertEqual(len(d), 3) self.assertEqual(d['a1'], 'xxxx') self.assertEqual(d['b1'], 'yyyy') self.assertEqual(d['c1'], 'zzzz') - d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_1) + d = self.kt_handle_bin.get_bulk(['a1', 'b1', 'c1'], db=DB_1, atomic=False) self.assertEqual(len(d), 0) def test_remove_bin(self): diff --git a/tests/t_simple_binary.py b/tests/t_simple_binary.py index a1e7fc4..4753d40 100644 --- a/tests/t_simple_binary.py +++ b/tests/t_simple_binary.py @@ -84,7 +84,7 @@ def test_set_bulk(self): 'k7': 111 } - n = self.kt_handle.set_bulk(dict) + n = self.kt_handle.set_bulk(dict, atomic=False) self.assertEqual(len(dict), n) self.assertEqual(self.kt_handle.get('k1'), 'one') self.assertEqual(self.kt_handle.get('k2'), 'two') @@ -95,15 +95,15 @@ def test_set_bulk(self): self.assertEqual(self.kt_handle.get('k7'), 111) d = self.kt_handle.get_bulk(['k1', 'k2', 'k3', 'k4', - 'k\n5', 'k\t6', 'k7']) + 'k\n5', 'k\t6', 'k7'], atomic=False) self.assertEqual(len(d), len(dict)) self.assertEqual(d, dict) self.assertEqual(self.kt_handle_http.count(), 7) - n = self.kt_handle.remove_bulk(['k1', 'k2', 'k\t6']) + n = self.kt_handle.remove_bulk(['k1', 'k2', 'k\t6'], atomic=False) self.assertEqual(self.kt_handle_http.count(), 4) - n = self.kt_handle.remove_bulk(['k3'], atomic=True) + n = self.kt_handle.remove_bulk(['k3'], atomic=False) self.assertEqual(self.kt_handle_http.count(), 3) def test_get_bulk(self): @@ -113,7 +113,7 @@ def test_get_bulk(self): self.assertTrue(self.kt_handle.set('c', 'three')) self.assertTrue(self.kt_handle.set('d', 'four')) - d = self.kt_handle.get_bulk(['a','b','c','d']) + d = self.kt_handle.get_bulk(['a','b','c','d'], atomic=False) assert d is not None self.assertEqual(d['a'], 'one') @@ -122,11 +122,11 @@ def test_get_bulk(self): self.assertEqual(d['d'], 'four') self.assertEqual(len(d), 4) - d = self.kt_handle.get_bulk(['a','x','y','d']) + d = self.kt_handle.get_bulk(['a','x','y','d'], atomic=False) self.assertEqual(len(d), 2) - d = self.kt_handle.get_bulk(['w','x','y','z']) + d = self.kt_handle.get_bulk(['w','x','y','z'], atomic=False) self.assertEqual(len(d), 0) - d = self.kt_handle.get_bulk([]) + d = self.kt_handle.get_bulk([], atomic=False) self.assertEqual(d, {}) def test_large_key(self): From fd532f224fe05bd1d0e4d2eb279919d503bf17c7 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Thu, 13 Feb 2014 18:36:05 +0000 Subject: [PATCH 48/62] Don't flag an error for empty keys, doing nothing is just fine. --- kyototycoon/kt_binary.py | 9 +++------ kyototycoon/kt_http.py | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 15dadfd..7dca033 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -97,8 +97,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): raise KyotoTycoonException('atomic supported under the HTTP procotol only') if isinstance(kv_dict, dict) and len(kv_dict) < 1: - self.err.set_error(self.err.LOGIC, 'no key:value pairs specified') - return 0 + return 0 # ...done if expire is None: expire = DEFAULT_EXPIRE @@ -129,8 +128,7 @@ def remove_bulk(self, keys, atomic, db): raise KyotoTycoonException('atomic supported under the HTTP procotol only') if len(keys) < 1: - self.err.set_error(self.err.LOGIC, 'no keys specified') - return 0 + return 0 # ...done if db is None: db = 0 @@ -161,8 +159,7 @@ def get_bulk(self, keys, atomic, db): raise KyotoTycoonException('atomic supported under the HTTP procotol only') if len(keys) < 1: - self.err.set_error(self.err.LOGIC, 'no keys specified') - return {} + return {} # ...done if db is None: db = 0 diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 4d18669..a2e4ef0 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -426,8 +426,7 @@ def get(self, key, db=None): def set_bulk(self, kv_dict, expire, atomic, db): if isinstance(kv_dict, dict) and len(kv_dict) < 1: - self.err.set_error(self.err.LOGIC, 'no key:value pairs specified') - return 0 + return 0 # ...done path = '/rpc/set_bulk' if db: @@ -453,8 +452,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): def remove_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC, 'no keys specified') - return 0 + return 0 # ...done path = '/rpc/remove_bulk' if db: @@ -483,8 +481,7 @@ def remove_bulk(self, keys, atomic, db): def get_bulk(self, keys, atomic, db): if len(keys) < 1: - self.err.set_error(self.err.LOGIC, 'no keys specified') - return {} + return {} # ...done path = '/rpc/get_bulk' if db: From f6872174368a028deaafec5e0088881490cba971 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 14 Feb 2014 11:34:51 +0000 Subject: [PATCH 49/62] Set proper atomic default depending on protocol used. --- kyototycoon/kyototycoon.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 2e3c533..7400aca 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -33,8 +33,14 @@ def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, ''' - protocol = kt_binary if binary else kt_http - self.core = protocol.ProtocolHandler(pack_type, custom_packer, exceptions) + if binary: + self.atomic = False # The binary protocol does not support atomic operations. + self.core = kt_binary.ProtocolHandler(pack_type, custom_packer, exceptions) + else: + self.atomic = True + self.core = kt_http.ProtocolHandler(pack_type, custom_packer, exceptions) + + def error(self): '''Return the error state from the last operation.''' @@ -126,20 +132,20 @@ def get_int(self, key, db=None): return self.core.get_int(key, db) - def set_bulk(self, kv_dict, expire=None, atomic=True, db=None): + def set_bulk(self, kv_dict, expire=None, atomic=None, db=None): '''Set the values for several records at once.''' - return self.core.set_bulk(kv_dict, expire, atomic, db) + return self.core.set_bulk(kv_dict, expire, self.atomic if atomic is None else atomic, db) - def remove_bulk(self, keys, atomic=True, db=None): + def remove_bulk(self, keys, atomic=None, db=None): '''Remove several records at once.''' - return self.core.remove_bulk(keys, atomic, db) + return self.core.remove_bulk(keys, self.atomic if atomic is None else atomic, db) - def get_bulk(self, keys, atomic=True, db=None): + def get_bulk(self, keys, atomic=None, db=None): '''Retrieve the values for several records at once.''' - return self.core.get_bulk(keys, atomic, db) + return self.core.get_bulk(keys, self.atomic if atomic is None else atomic, db) def vacuum(self, db=None): '''Scan the database and eliminate regions of expired records.''' From d7a23c4639aac7a7e0573b47a05a4600814de586 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 14 Feb 2014 11:35:25 +0000 Subject: [PATCH 50/62] Make compare-and-swap atomic. --- kyototycoon/kt_http.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index a2e4ef0..130c6d9 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -730,9 +730,8 @@ def append(self, key, value, expire, db): not isinstance(value, unicode_type)): raise ValueError('value is not a string or bytes type') - data = self.get(key) - if data is None: - data = type(value)() + old_data = self.get(key) + data = type(value)() if old_data is None else old_data if (not isinstance(data, bytes_type) and not isinstance(data, unicode_type)): @@ -747,7 +746,8 @@ def append(self, key, value, expire, db): data += value - if self.set(key, data, expire, db) is not True: + # This makes the operation atomic... + if self.cas(key, old_data, data, expire, db) is not True: self.err.set_error(self.err.EMISC, 'error while storing modified value') return False From 94d7b3b61f4b72213b4ae164f16936b29b3e350f Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sun, 16 Feb 2014 23:33:27 +0000 Subject: [PATCH 51/62] Make the KyotoTycoon object usable in 'with' statements. --- kyototycoon/kyototycoon.py | 7 ++++++ tests/kt_performance.py | 48 ++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 7400aca..100b850 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -40,7 +40,14 @@ def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, self.atomic = True self.core = kt_http.ProtocolHandler(pack_type, custom_packer, exceptions) + def __call__(self, *args, **kwargs): + return self if self.open(*args, **kwargs) else None + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() def error(self): '''Return the error state from the last operation.''' diff --git a/tests/kt_performance.py b/tests/kt_performance.py index fd710c8..54d2959 100644 --- a/tests/kt_performance.py +++ b/tests/kt_performance.py @@ -92,39 +92,37 @@ def main(): (KT_PACKER_JSON, "KT_PACKER_JSON"), (KT_PACKER_STRING, "KT_PACKER_STRING")): kt = KyotoTycoon(binary=binary, pack_type=packer_type) - kt.open(server["host"], server["port"], timeout=2) - start = time() - bad = 0 + with kt(server["host"], server["port"], timeout=2) as db: + start = time() + bad = 0 - for i in range(NUM_ITERATIONS): - if is_unicode: - key = "key-%d-u-%d-%d-café" % (binary, packer_type, i) - value = "value-%d-u-%d-%d-café" % (binary, packer_type, i) - else: - key = "key-%d-s-%d-%d-cafe" % (binary, packer_type, i) - value = "value-%d-s-%d-%d-cafe" % (binary, packer_type, i) + for i in range(NUM_ITERATIONS): + if is_unicode: + key = "key-%d-u-%d-%d-café" % (binary, packer_type, i) + value = "value-%d-u-%d-%d-café" % (binary, packer_type, i) + else: + key = "key-%d-s-%d-%d-cafe" % (binary, packer_type, i) + value = "value-%d-s-%d-%d-cafe" % (binary, packer_type, i) - kt.set(key, value) - output = kt.get(key) + db.set(key, value) + output = db.get(key) - if output != value: - bad += 1 + if output != value: + bad += 1 - kt.remove(key) - output = kt.get(key) + db.remove(key) + output = db.get(key) - if output is not None: - bad += 1 + if output is not None: + bad += 1 - duration = time() - start - rate = NUM_ITERATIONS / duration + duration = time() - start + rate = NUM_ITERATIONS / duration - print("%-15s | %-16s | %-16s | %5.2f s | %10.2f ips | %-6s" % - (binary, is_unicode, packer_name, duration, rate, - "No" if bad > 0 else "Yes")) - - kt.close() + print("%-15s | %-16s | %-16s | %5.2f s | %10.2f ips | %-6s" % + (binary, is_unicode, packer_name, duration, rate, + "No" if bad > 0 else "Yes")) if __name__ == "__main__": From 4eb1aa89f45c1b3df2eccbb283c50290e690e4eb Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Mon, 17 Feb 2014 00:05:35 +0000 Subject: [PATCH 52/62] Added 'connect()' method to make 'with' statements more explicit. --- kyototycoon/kyototycoon.py | 16 ++++++++++++---- tests/kt_performance.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 100b850..cada5f3 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -40,9 +40,6 @@ def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, self.atomic = True self.core = kt_http.ProtocolHandler(pack_type, custom_packer, exceptions) - def __call__(self, *args, **kwargs): - return self if self.open(*args, **kwargs) else None - def __enter__(self): return self @@ -57,7 +54,18 @@ def error(self): def open(self, host=KT_DEFAULT_HOST, port=KT_DEFAULT_PORT, timeout=KT_DEFAULT_TIMEOUT): '''Open a new connection to a KT server.''' - return self.core.open(host, port, timeout) + return True if self.core.open(host, port, timeout) else False + + def connect(self, *args, **kwargs): + ''' + Open a new connection to a KT server. + + The same as "open()" but returning "self" instead of a boolean, allowing + KyotoTycoon objects to be used as context managers in "with" statements. + + ''' + + return self if self.open(*args, **kwargs) else None def close(self): '''Close an open connection to the KT server.''' diff --git a/tests/kt_performance.py b/tests/kt_performance.py index 54d2959..c762ed5 100644 --- a/tests/kt_performance.py +++ b/tests/kt_performance.py @@ -93,7 +93,7 @@ def main(): (KT_PACKER_STRING, "KT_PACKER_STRING")): kt = KyotoTycoon(binary=binary, pack_type=packer_type) - with kt(server["host"], server["port"], timeout=2) as db: + with kt.connect(server["host"], server["port"], timeout=2) as db: start = time() bad = 0 From 5e608792e5b13cd5aae03509a10606747775a49f Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Sat, 22 Feb 2014 15:10:28 +0000 Subject: [PATCH 53/62] Rename README to reflect format. --- README => README.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.rst (100%) diff --git a/README b/README.rst similarity index 100% rename from README rename to README.rst From 8b23be1eafb22631ae0a2965194f777898592209 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Thu, 6 Mar 2014 12:14:28 +0000 Subject: [PATCH 54/62] Do not depend on simplejson anymore. Proper benchmarks with recent Python releases show no difference. --- kyototycoon/kt_binary.py | 5 +---- kyototycoon/kt_http.py | 5 +---- setup.py | 4 +--- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 7dca033..f8c3e07 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -25,10 +25,7 @@ except ImportError: import pickle -try: - import simplejson as json -except ImportError: - import json +import json MB_SET_BULK = 0xb8 MB_GET_BULK = 0xba diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 130c6d9..ebe5ea2 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -40,10 +40,7 @@ except ImportError: import pickle -try: - import simplejson as json -except ImportError: - import json +import json KT_HTTP_HEADER = {'Content-Type' : 'text/tab-separated-values; colenc=U'} diff --git a/setup.py b/setup.py index b76aefe..c15097b 100644 --- a/setup.py +++ b/setup.py @@ -14,11 +14,9 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.7', + version='0.5.8', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], - requires=['simplejson'], url='https://github.com/carlosefr/python-kyototycoon', - zip_safe=False ) From 80105de943716a2c90bd21a26cd252f95c0f9b03 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 7 Mar 2014 10:58:22 +0000 Subject: [PATCH 55/62] Default database to 0. Do not clobber python keywords. --- kyototycoon/kt_binary.py | 40 +++++++++++++-------------- kyototycoon/kt_http.py | 56 +++++++++++++++++++------------------- kyototycoon/kyototycoon.py | 44 +++++++++++++++--------------- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index f8c3e07..0f86ee9 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -85,11 +85,11 @@ def close(self): self.socket.close() return True - def get(self, key, db=None): + def get(self, key, db=0): values = self.get_bulk([key], False, db) return values[key] if values else None - def set_bulk(self, kv_dict, expire, atomic, db): + def set_bulk(self, kv_dict, expire, atomic, db=0): if atomic: raise KyotoTycoonException('atomic supported under the HTTP procotol only') @@ -120,7 +120,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): self.err.set_success() return num_items - def remove_bulk(self, keys, atomic, db): + def remove_bulk(self, keys, atomic, db=0): if atomic: raise KyotoTycoonException('atomic supported under the HTTP procotol only') @@ -151,7 +151,7 @@ def remove_bulk(self, keys, atomic, db): return num_items - def get_bulk(self, keys, atomic, db): + def get_bulk(self, keys, atomic, db=0): if atomic: raise KyotoTycoonException('atomic supported under the HTTP procotol only') @@ -189,57 +189,57 @@ def get_bulk(self, keys, atomic, db): return items - def get_int(self, key, db=None): + def get_int(self, key, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def vacuum(self, db): + def vacuum(self, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def match_prefix(self, prefix, max, db): + def match_prefix(self, prefix, limit, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def match_regex(self, regex, max, db): + def match_regex(self, regex, limit, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def set(self, key, value, expire, db): + def set(self, key, value, expire, db=0): numitems = self.set_bulk({key: value}, expire, False, db) return numitems > 0 - def add(self, key, value, expire, db): + def add(self, key, value, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def cas(self, key, old_val, new_val, expire, db): + def cas(self, key, old_val, new_val, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def remove(self, key, db): + def remove(self, key, db=0): numitems = self.remove_bulk([key], False, db) return numitems > 0 - def replace(self, key, value, expire, db): + def replace(self, key, value, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def append(self, key, value, expire, db): + def append(self, key, value, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def increment(self, key, delta, expire, db): + def increment(self, key, delta, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def increment_double(self, key, delta, expire, db): + def increment_double(self, key, delta, expire, db=0): raise NotImplementedError('supported under the HTTP procotol only') def report(self): raise NotImplementedError('supported under the HTTP procotol only') - def status(self, db=None): + def status(self, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def clear(self, db=None): + def clear(self, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def count(self, db=None): + def count(self, db=0): raise NotImplementedError('supported under the HTTP procotol only') - def size(self, db=None): + def size(self, db=0): raise NotImplementedError('supported under the HTTP procotol only') def play_script(self, name, kv_dict=None): diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index ebe5ea2..019bd2a 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -44,9 +44,9 @@ KT_HTTP_HEADER = {'Content-Type' : 'text/tab-separated-values; colenc=U'} -def _dict_to_tsv(dict): +def _dict_to_tsv(kv_dict): lines = [] - for k, v in dict.items(): + for k, v in kv_dict.items(): quoted = quote_from_bytes(v) if isinstance(v, bytes) else quote(str(v)) lines.append('%s\t%s' % (quote(k.encode('utf-8')), quoted)) return '\n'.join(lines) @@ -103,7 +103,7 @@ def __exit__(self, type, value, tb): # Cleanup the cursor when leaving "with" blocks... self.delete() - def jump(self, key=None, db=None): + def jump(self, key=None, db=0): '''Jump the cursor to a record (first record if "None") for forward scan.''' path = '/rpc/cur_jump' @@ -126,7 +126,7 @@ def jump(self, key=None, db=None): self.err.set_success() return True - def jump_back(self, key=None, db=None): + def jump_back(self, key=None, db=0): '''Jump the cursor to a record (last record if "None") for forward scan.''' path = '/rpc/cur_jump_back' @@ -401,7 +401,7 @@ def echo(self): self.err.set_success() return True - def get(self, key, db=None): + def get(self, key, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -421,7 +421,7 @@ def get(self, key, db=None): self.err.set_success() return self.unpack(body) - def set_bulk(self, kv_dict, expire, atomic, db): + def set_bulk(self, kv_dict, expire, atomic, db=0): if isinstance(kv_dict, dict) and len(kv_dict) < 1: return 0 # ...done @@ -447,7 +447,7 @@ def set_bulk(self, kv_dict, expire, atomic, db): self.err.set_success() return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) - def remove_bulk(self, keys, atomic, db): + def remove_bulk(self, keys, atomic, db=0): if len(keys) < 1: return 0 # ...done @@ -476,7 +476,7 @@ def remove_bulk(self, keys, atomic, db): return count - def get_bulk(self, keys, atomic, db): + def get_bulk(self, keys, atomic, db=0): if len(keys) < 1: return {} # ...done @@ -512,7 +512,7 @@ def get_bulk(self, keys, atomic, db): self.err.set_success() return rv - def get_int(self, key, db=None): + def get_int(self, key, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -528,7 +528,7 @@ def get_int(self, key, db=None): self.err.set_success() return struct.unpack('>q', body)[0] - def vacuum(self, db): + def vacuum(self, db=0): path = '/rpc/vacuum' if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -544,7 +544,7 @@ def vacuum(self, db): self.err.set_success() return True - def match_prefix(self, prefix, max, db): + def match_prefix(self, prefix, limit, db=0): if prefix is None: self.err.set_error(self.err.LOGIC, 'no key prefix specified') return None @@ -555,8 +555,8 @@ def match_prefix(self, prefix, max, db): path += '?DB=' + db request_dict = {'prefix': prefix.encode('utf-8')} - if max: - request_dict['max'] = max + if limit: + request_dict['max'] = limit request_body = _dict_to_tsv(request_dict) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -582,7 +582,7 @@ def match_prefix(self, prefix, max, db): self.err.set_success() return rv - def match_regex(self, regex, max, db): + def match_regex(self, regex, limit, db=0): if regex is None: self.err.set_error(self.err.LOGIC, 'no regular expression specified') return None @@ -593,8 +593,8 @@ def match_regex(self, regex, max, db): path += '?DB=' + db request_dict = {'regex': regex.encode('utf-8')} - if max: - request_dict['max'] = max + if limit: + request_dict['max'] = limit request_body = _dict_to_tsv(request_dict) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -620,7 +620,7 @@ def match_regex(self, regex, max, db): self.err.set_success() return rv - def set(self, key, value, expire, db): + def set(self, key, value, expire, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -635,7 +635,7 @@ def set(self, key, value, expire, db): self.err.set_success() return True - def add(self, key, value, expire, db): + def add(self, key, value, expire, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -650,7 +650,7 @@ def add(self, key, value, expire, db): self.err.set_success() return True - def cas(self, key, old_val, new_val, expire, db): + def cas(self, key, old_val, new_val, expire, db=0): if old_val is None and new_val is None: self.err.set_error(self.err.LOGIC, 'old value and/or new value must be specified') return False @@ -683,7 +683,7 @@ def cas(self, key, old_val, new_val, expire, db): self.err.set_success() return True - def remove(self, key, db): + def remove(self, key, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -699,7 +699,7 @@ def remove(self, key, db): self.err.set_success() return True - def replace(self, key, value, expire, db): + def replace(self, key, value, expire, db=0): key = quote(key.encode('utf-8')) if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -714,7 +714,7 @@ def replace(self, key, value, expire, db): self.err.set_success() return True - def append(self, key, value, expire, db): + def append(self, key, value, expire, db=0): # Simultaneous support for Python 2/3 makes this cumbersome... if sys.version_info[0] >= 3: bytes_type = bytes @@ -751,7 +751,7 @@ def append(self, key, value, expire, db): self.err.set_success() return True - def increment(self, key, delta, expire, db): + def increment(self, key, delta, expire, db=0): path = '/rpc/increment' if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -768,7 +768,7 @@ def increment(self, key, delta, expire, db): self.err.set_success() return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) - def increment_double(self, key, delta, expire, db): + def increment_double(self, key, delta, expire, db=0): if key is None: self.err.set_error(self.err.LOGIC, 'no key specified') return False @@ -804,7 +804,7 @@ def report(self): self.err.set_success() return report_dict - def status(self, db=None): + def status(self, db=0): path = '/rpc/status' if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -824,7 +824,7 @@ def status(self, db=None): self.err.set_success() return status_dict - def clear(self, db=None): + def clear(self, db=0): path = '/rpc/clear' if db: db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) @@ -839,11 +839,11 @@ def clear(self, db=None): self.err.set_success() return True - def count(self, db=None): + def count(self, db=0): st = self.status(db) return None if st is None else int(st['count']) - def size(self, db=None): + def size(self, db=0): st = self.status(db) return None if st is None else int(st['size']) diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index cada5f3..88da5fa 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -77,105 +77,105 @@ def report(self): return self.core.report() - def status(self, db=None): + def status(self, db=0): '''Get status information for the database.''' return self.core.status(db) - def clear(self, db=None): + def clear(self, db=0): '''Remove all records in the database.''' return self.core.clear(db) - def count(self, db=None): + def count(self, db=0): '''Number of records in the database.''' return self.core.count(db) - def size(self, db=None): + def size(self, db=0): '''Current database size (in bytes).''' return self.core.size(db) - def set(self, key, value, expire=None, db=None): + def set(self, key, value, expire=None, db=0): '''Set the value for a record.''' return self.core.set(key, value, expire, db) - def add(self, key, value, expire=None, db=None): + def add(self, key, value, expire=None, db=0): '''Set the value for a record (does nothing if the record already exists).''' return self.core.add(key, value, expire, db) - def replace(self, key, value, expire=None, db=None): + def replace(self, key, value, expire=None, db=0): '''Replace the value of an existing record.''' return self.core.replace(key, value, expire, db) - def append(self, key, value, expire=None, db=None): + def append(self, key, value, expire=None, db=0): '''Append "value" to the string value of a record.''' return self.core.append(key, value, expire, db) - def increment(self, key, delta, expire=None, db=None): + def increment(self, key, delta, expire=None, db=0): '''Add "delta" to the numeric integer value of a record.''' return self.core.increment(key, delta, expire, db) - def increment_double(self, key, delta, expire=None, db=None): + def increment_double(self, key, delta, expire=None, db=0): '''Add "delta" to the numeric double value of a record.''' return self.core.increment_double(key, delta, expire, db) - def cas(self, key, old_val=None, new_val=None, expire=None, db=None): + def cas(self, key, old_val=None, new_val=None, expire=None, db=0): '''If the old value of a record is "old_val", replace it with "new_val".''' return self.core.cas(key, old_val, new_val, expire, db) - def remove(self, key, db=None): + def remove(self, key, db=0): '''Remove a record.''' return self.core.remove(key, db) - def get(self, key, db=None): + def get(self, key, db=0): '''Retrieve the value for a record.''' return self.core.get(key, db) - def get_int(self, key, db=None): + def get_int(self, key, db=0): '''Retrieve the numeric integer value for a record.''' return self.core.get_int(key, db) - def set_bulk(self, kv_dict, expire=None, atomic=None, db=None): + def set_bulk(self, kv_dict, expire=None, atomic=None, db=0): '''Set the values for several records at once.''' return self.core.set_bulk(kv_dict, expire, self.atomic if atomic is None else atomic, db) - def remove_bulk(self, keys, atomic=None, db=None): + def remove_bulk(self, keys, atomic=None, db=0): '''Remove several records at once.''' return self.core.remove_bulk(keys, self.atomic if atomic is None else atomic, db) - def get_bulk(self, keys, atomic=None, db=None): + def get_bulk(self, keys, atomic=None, db=0): '''Retrieve the values for several records at once.''' return self.core.get_bulk(keys, self.atomic if atomic is None else atomic, db) - def vacuum(self, db=None): + def vacuum(self, db=0): '''Scan the database and eliminate regions of expired records.''' return self.core.vacuum(db) - def match_prefix(self, prefix, max=None, db=None): + def match_prefix(self, prefix, limit=None, db=0): '''Get keys matching a prefix string.''' - return self.core.match_prefix(prefix, max, db) + return self.core.match_prefix(prefix, limit, db) - def match_regex(self, regex, max=None, db=None): + def match_regex(self, regex, limit=None, db=0): '''Get keys matching a ragular expression string.''' - return self.core.match_regex(regex, max, db) + return self.core.match_regex(regex, limit, db) def cursor(self): '''Obtain a new (uninitialized) record cursor.''' From e89f2ec51c66bc221dfc1ea287365e6d82b4b898 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 7 Mar 2014 11:37:16 +0000 Subject: [PATCH 56/62] Always include DB in path. --- kyototycoon/kt_http.py | 145 +++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 92 deletions(-) diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 019bd2a..99ad5d8 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -51,7 +51,7 @@ def _dict_to_tsv(kv_dict): lines.append('%s\t%s' % (quote(k.encode('utf-8')), quoted)) return '\n'.join(lines) -def _content_type_decoder(content_type=''): +def _content_type_decoder(content_type): '''Select the appropriate decoding function to use based on the response headers.''' if content_type.endswith('colenc=B'): @@ -62,7 +62,7 @@ def _content_type_decoder(content_type=''): return lambda x: x -def _tsv_to_dict(tsv_str, content_type=''): +def _tsv_to_dict(tsv_str, content_type): decode = _content_type_decoder(content_type) rv = {} @@ -72,15 +72,14 @@ def _tsv_to_dict(tsv_str, content_type=''): rv[decode(kv[0])] = decode(kv[1]) return rv -def _tsv_to_list(tsv_str, content_type=''): +def _tsv_to_list(tsv_str, content_type): decode = _content_type_decoder(content_type) rv = [] for row in tsv_str.split(b'\n'): kv = row.split(b'\t') if len(kv) == 2: - pair = (decode(kv[0]), decode(kv[1])) - rv.append(pair) + rv.append([decode(kv[0]), decode(kv[1])]) return rv @@ -99,17 +98,15 @@ def __init__(self, protocol_handler): def __enter__(self): return self - def __exit__(self, type, value, tb): + def __exit__(self, type, value, traceback): # Cleanup the cursor when leaving "with" blocks... self.delete() def jump(self, key=None, db=0): '''Jump the cursor to a record (first record if "None") for forward scan.''' - path = '/rpc/cur_jump' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/cur_jump?DB=' + db request_dict = {'CUR': self.cursor_id} if key: @@ -129,10 +126,8 @@ def jump(self, key=None, db=0): def jump_back(self, key=None, db=0): '''Jump the cursor to a record (last record if "None") for forward scan.''' - path = '/rpc/cur_jump_back' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path += '/rpc/cur_jump_back?DB=' + db request_dict = {'CUR': self.cursor_id} if key: @@ -402,12 +397,10 @@ def echo(self): return True def get(self, key, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) - self.conn.request('GET', key) + self.conn.request('GET', path) res, body = self.getresponse() if res.status == 404: @@ -425,17 +418,15 @@ def set_bulk(self, kv_dict, expire, atomic, db=0): if isinstance(kv_dict, dict) and len(kv_dict) < 1: return 0 # ...done - path = '/rpc/set_bulk' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/set_bulk?DB=' + db request_body = ['atomic\t\n' if atomic else ''] - for k, v in kv_dict.items(): - k = quote(k.encode('utf-8')) - v = quote(self.pack(v)) - request_body.append('_%s\t%s\n' % (k, v)) + for key, value in kv_dict.items(): + key = quote(key.encode('utf-8')) + value = quote(self.pack(value)) + request_body.append('_%s\t%s\n' % (key, value)) self.conn.request('POST', path, body=''.join(request_body), headers=KT_HTTP_HEADER) @@ -451,10 +442,8 @@ def remove_bulk(self, keys, atomic, db=0): if len(keys) < 1: return 0 # ...done - path = '/rpc/remove_bulk' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/remove_bulk?DB=' + db request_body = ['atomic\t\n' if atomic else ''] @@ -480,10 +469,8 @@ def get_bulk(self, keys, atomic, db=0): if len(keys) < 1: return {} # ...done - path = '/rpc/get_bulk' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/get_bulk?DB=' + db request_body = ['atomic\t\n' if atomic else ''] @@ -513,12 +500,10 @@ def get_bulk(self, keys, atomic, db=0): return rv def get_int(self, key, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) - self.conn.request('GET', key) + self.conn.request('GET', path) res, body = self.getresponse() if res.status != 200: @@ -529,10 +514,8 @@ def get_int(self, key, db=0): return struct.unpack('>q', body)[0] def vacuum(self, db=0): - path = '/rpc/vacuum' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/vacuum?DB=' + db self.conn.request('GET', path) @@ -549,10 +532,8 @@ def match_prefix(self, prefix, limit, db=0): self.err.set_error(self.err.LOGIC, 'no key prefix specified') return None - path = '/rpc/match_prefix' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/match_prefix?DB=' + db request_dict = {'prefix': prefix.encode('utf-8')} if limit: @@ -587,10 +568,8 @@ def match_regex(self, regex, limit, db=0): self.err.set_error(self.err.LOGIC, 'no regular expression specified') return None - path = '/rpc/match_regex' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/match_regex?DB=' + db request_dict = {'regex': regex.encode('utf-8')} if limit: @@ -621,13 +600,11 @@ def match_regex(self, regex, limit, db=0): return rv def set(self, key, value, expire, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) value = self.pack(value) - status = self._rest_put('set', key, value, expire) + status = self._rest_put('set', path, value, expire) if status != 201: self.err.set_error(self.err.EMISC) return False @@ -636,13 +613,11 @@ def set(self, key, value, expire, db=0): return True def add(self, key, value, expire, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) value = self.pack(value) - status = self._rest_put('add', key, value, expire) + status = self._rest_put('add', path, value, expire) if status != 201: self.err.set_error(self.err.EMISC) return False @@ -655,10 +630,8 @@ def cas(self, key, old_val, new_val, expire, db=0): self.err.set_error(self.err.LOGIC, 'old value and/or new value must be specified') return False - path = '/rpc/cas' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/cas?DB=' + db request_dict = {'key': key.encode('utf-8')} @@ -684,12 +657,10 @@ def cas(self, key, old_val, new_val, expire, db=0): return True def remove(self, key, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) - self.conn.request('DELETE', key) + self.conn.request('DELETE', path) res, body = self.getresponse() if res.status != 204: @@ -700,13 +671,11 @@ def remove(self, key, db=0): return True def replace(self, key, value, expire, db=0): - key = quote(key.encode('utf-8')) - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - key = '/%s/%s' % (db, key) + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/%s/%s' % (db, quote(key.encode('utf-8'))) value = self.pack(value) - status = self._rest_put('replace', key, value, expire) + status = self._rest_put('replace', path, value, expire) if status != 201: self.err.set_error(self.err.NOTFOUND) return False @@ -752,10 +721,8 @@ def append(self, key, value, expire, db=0): return True def increment(self, key, delta, expire, db=0): - path = '/rpc/increment' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/increment?DB=' + db request_body = 'key\t%s\nnum\t%d\n' % (key, delta) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -773,10 +740,8 @@ def increment_double(self, key, delta, expire, db=0): self.err.set_error(self.err.LOGIC, 'no key specified') return False - path = '/rpc/increment_double' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/increment_double?DB=' + db request_body = 'key\t%s\nnum\t%f\n' % (key, delta) self.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) @@ -805,10 +770,8 @@ def report(self): return report_dict def status(self, db=0): - path = '/rpc/status' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/status?DB=' + db self.conn.request('GET', path) res, body = self.getresponse() @@ -825,10 +788,8 @@ def status(self, db=0): return status_dict def clear(self, db=0): - path = '/rpc/clear' - if db: - db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) - path += '?DB=' + db + db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) + path = '/rpc/clear?DB=' + db self.conn.request('GET', path) res, body = self.getresponse() From 9410d032292b64109f8fbb0ede076267ff7dac4c Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 7 Mar 2014 11:41:19 +0000 Subject: [PATCH 57/62] Remove redundant database checks. --- kyototycoon/kt_binary.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 0f86ee9..fb815f0 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -99,9 +99,6 @@ def set_bulk(self, kv_dict, expire, atomic, db=0): if expire is None: expire = DEFAULT_EXPIRE - if db is None: - db = 0 - request = [struct.pack('!BII', MB_SET_BULK, 0, len(kv_dict))] for key, value in kv_dict.items(): @@ -127,9 +124,6 @@ def remove_bulk(self, keys, atomic, db=0): if len(keys) < 1: return 0 # ...done - if db is None: - db = 0 - request = [struct.pack('!BII', MB_REMOVE_BULK, 0, len(keys))] for key in keys: @@ -158,9 +152,6 @@ def get_bulk(self, keys, atomic, db=0): if len(keys) < 1: return {} # ...done - if db is None: - db = 0 - request = [struct.pack('!BII', MB_GET_BULK, 0, len(keys))] for key in keys: From b0379267c43efa6b6c8b23a482717c7ddefbb24b Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 7 Mar 2014 11:43:41 +0000 Subject: [PATCH 58/62] Bump version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c15097b..c4721b6 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.8', + version='0.5.9', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], From f373e764ac9f960ac4a5c0864c5115a755ee8cdb Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 19 Mar 2014 11:21:46 +0000 Subject: [PATCH 59/62] Updated README to make clear that this is a compatibility-breaking fork. --- README.rst | 56 +++++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/README.rst b/README.rst index 8837868..7dfc38f 100644 --- a/README.rst +++ b/README.rst @@ -1,25 +1,17 @@ ABOUT ----- -This is a native Python client library for Kyoto Tycoon. -For more information on Kyoto Tycoon please refer to the -official project website: +This is a native Python client library for the Kyoto Tycoon server, +supporting both Python 2 and 3. It includes significant performance +improvements and bug fixes over the (now seemingly unmaintained) +original library by Toru Maesaka and Stephen Hamer. - http://fallabs.com/kyototycoon/ - -This library's interface follows the preferred interface -provided by Kyoto Tycoon's original author(s): - - http://fallabs.com/kyototycoon/kyototycoon.idl +For more information on Kyoto Tycoon server, please refer to: -There is currently no documentation for this software but, in -the meantime, please see the file "kyototycoon.py" for the -interface. + http://fallabs.com/kyototycoon/ -This library includes cursor support and significant performance -improvements over the (now unmaintained) original library by -Toru Maesaka. It also supports unicode keys and works with both -Python 2 and 3. +FEATURES +-------- The more efficient binary protocol is also supported along with the HTTP protocol. It provides a performance improvement of up to 6x, but only the following operations are available: @@ -59,6 +51,18 @@ once. The caller will most likely know the type of data that the called script returns and must do the marshalling itself. +COMPATIBILITY +------------- +This library is still not at version 1.0, which means the API and +behavior are not guaranteed to remain consistent between versions. + +Support for using an error object has been removed. If you need +the old behavior for compatibility reasons, use a version up to +(and including) v0.5.9. Versions later than this raise exceptions +on all Kyoto Tycoon errors. + + + INSTALLATION ------------ You can install the latest version of this library from source:: @@ -66,26 +70,6 @@ You can install the latest version of this library from source:: python setup.py build python setup.py install -A version of this library is available from PyPI, although it may -not always be the latest and greatest version. You can find it at: - - https://pypi.python.org/pypi/python-kyototycoon/ - -You can install packages directly from PyPI using ``pip``:: - - pip install python-kyototycoon - -This library is still not at version 1.0, which means the API and -behavior are not guaranteed to remain consistent between versions. -If you require a specific version consider using versioning with -``pip``. For example:: - - pip install python-kyototycoon==0.4.6 - -This is ideal for use with any automatic scripts you may be using -to build/deploy your application. - - AUTHORS ------- * Toru Maesaka From 3393ee340208fa3fc28a712ae3e8d8006e979d99 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 19 Mar 2014 12:21:12 +0000 Subject: [PATCH 60/62] All errors are reported through exceptions now. Adapted unit tests to match. --- kyototycoon/__init__.py | 5 +- kyototycoon/kt_binary.py | 37 +++----- kyototycoon/kt_error.py | 85 +----------------- kyototycoon/kt_http.py | 175 +++++++++++-------------------------- kyototycoon/kyototycoon.py | 27 +++--- tests/t_arithmetic.py | 5 +- tests/t_multi.py | 25 +++--- tests/t_simple.py | 39 +-------- tests/t_simple_binary.py | 15 ---- 9 files changed, 90 insertions(+), 323 deletions(-) diff --git a/kyototycoon/__init__.py b/kyototycoon/__init__.py index cf7eb81..f18f936 100644 --- a/kyototycoon/__init__.py +++ b/kyototycoon/__init__.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -from .kyototycoon import KyotoTycoon, \ - KT_DEFAULT_HOST, \ - KT_DEFAULT_PORT, \ - KT_DEFAULT_TIMEOUT +from .kyototycoon import KyotoTycoon from .kt_common import KT_PACKER_CUSTOM, \ KT_PACKER_PICKLE, \ diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index fb815f0..137a709 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -12,7 +12,7 @@ import socket import struct -from .kt_error import KyotoTycoonError, KyotoTycoonException +from .kt_error import KyotoTycoonException from .kt_common import KT_PACKER_CUSTOM, \ KT_PACKER_PICKLE, \ @@ -36,8 +36,7 @@ DEFAULT_EXPIRE = 0x7fffffffffffffff class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): - self.err = KyotoTycoonError(exceptions) + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): self.socket = None if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: @@ -110,12 +109,10 @@ def set_bulk(self, kv_dict, expire, atomic, db=0): magic, = struct.unpack('!B', self._read(1)) if magic != MB_SET_BULK: - self.err.set_error(self.err.INTERNAL) - return False + raise KyotoTycoonException('bad response [%s]' % hex(magic)) - num_items, = struct.unpack('!I', self._read(4)) - self.err.set_success() - return num_items + # Number of items set... + return struct.unpack('!I', self._read(4))[0] def remove_bulk(self, keys, atomic, db=0): if atomic: @@ -134,16 +131,10 @@ def remove_bulk(self, keys, atomic, db=0): magic, = struct.unpack('!B', self._read(1)) if magic != MB_REMOVE_BULK: - self.err.set_error(self.err.INTERNAL) - return False - - num_items, = struct.unpack('!I', self._read(4)) - if num_items > 0: - self.err.set_success() - else: - self.err.set_error(self.err.NOTFOUND) + raise KyotoTycoonException('bad response [%s]' % hex(magic)) - return num_items + # Number of items removed... + return struct.unpack('!I', self._read(4))[0] def get_bulk(self, keys, atomic, db=0): if atomic: @@ -162,8 +153,7 @@ def get_bulk(self, keys, atomic, db=0): magic, = struct.unpack('!B', self._read(1)) if magic != MB_GET_BULK: - self.err.set_error(self.err.INTERNAL) - return False + raise KyotoTycoonException('bad response [%s]' % hex(magic)) num_items, = struct.unpack('!I', self._read(4)) items = {} @@ -173,11 +163,6 @@ def get_bulk(self, keys, atomic, db=0): value = self._read(value_length) items[key.decode('utf-8')] = self.unpack(value) - if num_items > 0: - self.err.set_success() - else: - self.err.set_error(self.err.NOTFOUND) - return items def get_int(self, key, db=0): @@ -251,8 +236,7 @@ def play_script(self, name, kv_dict=None): magic, = struct.unpack('!B', self._read(1)) if magic != MB_PLAY_SCRIPT: - self.err.set_error(self.err.INTERNAL) - return False + raise KyotoTycoonException('bad response [%s]' % hex(magic)) num_items, = struct.unpack('!I', self._read(4)) items = {} @@ -262,7 +246,6 @@ def play_script(self, name, kv_dict=None): value = self._read(value_length) items[key.decode('utf-8')] = value - self.err.set_success() return items def _write(self, data): diff --git a/kyototycoon/kt_error.py b/kyototycoon/kt_error.py index 06a3639..51c373f 100644 --- a/kyototycoon/kt_error.py +++ b/kyototycoon/kt_error.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2011, Toru Maesaka +# Copyright 2014, Carlos Rodrigues # # Redistribution and use of this source code is licensed under # the BSD license. See COPYING file for license description. @@ -8,87 +8,4 @@ class KyotoTycoonException(Exception): pass -class KyotoTycoonError(object): - SUCCESS = 0 - NOIMPL = 1 - INVALID = 2 - LOGIC = 3 - INTERNAL = 4 - NETWORK = 5 - NOTFOUND = 6 - EMISC = 255 - - ErrorNameDict = { - SUCCESS: "SUCCESS", - NOIMPL: "UNIMPLEMENTED", - INVALID: "INVALID", - LOGIC: "LOGIC", - INTERNAL: "INTERNAL", - NETWORK: "NETWORK", - NOTFOUND: "NOTFOUND", - EMISC: "EMISC", - } - - ErrorMessageDict = { - SUCCESS: "Operation Successful", - NOIMPL: "Unimplemented Operation", - INVALID: "Invalid Operation", - LOGIC: "Logic Error", - INTERNAL: "Internal Error", - NETWORK: "Network Error", - NOTFOUND: "Record Not Found", - EMISC: "Miscellaneous Error", - } - - def __init__(self, exceptions=False): - ''' - Initialize the last database operation error object. - - The "exceptions" parameter controls whether an exception is raised when - the library sets the error state to something that's an actual error. - - ''' - - self.exceptions = exceptions - self.set_success() - - def set_error(self, code, detail_message=None): - '''Set the error state for the last database operation.''' - - self.error_code = code - self.error_name = self.ErrorNameDict[code] - self.error_message = self.ErrorMessageDict[code] - self.error_detail = detail_message - - if self.exceptions and not self.ok(): - raise KyotoTycoonException(self.message()) - - def set_success(self): - '''Flag the last database operation as having been successful.''' - - self.set_error(self.SUCCESS) - - def ok(self): - '''Return whether the last database operation resulted in an error.''' - - return self.error_code in (self.SUCCESS, self.NOTFOUND) - - def code(self): - '''Return the error code for the last database operation.''' - - return self.error_code - - def name(self): - '''Return the error name for the last database operation.''' - - return self.error_name - - def message(self): - '''Return the error description for the last database operation.''' - - if self.error_detail: - return '%s (%s)' % (self.error_message, self.error_detail) - else: - return self.error_message - # EOF - kt_error.py diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 99ad5d8..67750bb 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -10,7 +10,7 @@ import time import sys -from .kt_error import KyotoTycoonError, KyotoTycoonException +from .kt_error import KyotoTycoonException from .kt_common import KT_PACKER_CUSTOM, \ KT_PACKER_PICKLE, \ @@ -91,7 +91,6 @@ def __init__(self, protocol_handler): self.cursor_id = Cursor.cursor_id_counter Cursor.cursor_id_counter += 1 - self.err = KyotoTycoonError() self.pack = self.protocol_handler.pack self.unpack = self.protocol_handler.unpack @@ -117,10 +116,8 @@ def jump(self, key=None, db=0): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def jump_back(self, key=None, db=0): @@ -138,10 +135,8 @@ def jump_back(self, key=None, db=0): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def step(self): @@ -153,11 +148,13 @@ def step(self): self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() - if res.status != 200: - self.err.set_error(self.err.EMISC) + if res.status == 450: + # Since this is normal while iterating, do not raise an exception... return False - self.err.set_success() + if res.status != 200: + raise KyotoTycoonException('protocol error [%d]' % res.status) + return True def step_back(self): @@ -169,11 +166,13 @@ def step_back(self): self.protocol_handler.conn.request('POST', path, body=request_body, headers=KT_HTTP_HEADER) res, body = self.protocol_handler.getresponse() - if res.status != 200: - self.err.set_error(self.err.EMISC) + if res.status == 450: + # Since this is normal while iterating, do not raise an exception... return False - self.err.set_success() + if res.status != 200: + raise KyotoTycoonException('protocol error [%d]' % res.status) + return True def set_value(self, value, step=False, expire=None): @@ -193,10 +192,8 @@ def set_value(self, value, step=False, expire=None): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def remove(self): @@ -209,10 +206,8 @@ def remove(self): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def get_key(self, step=False): @@ -229,10 +224,8 @@ def get_key(self, step=False): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return _tsv_to_dict(body, res.getheader('Content-Type', ''))[b'key'].decode('utf-8') def get_value(self, step=False): @@ -249,10 +242,8 @@ def get_value(self, step=False): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return self.unpack(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'value']) def get(self, step=False): @@ -269,18 +260,15 @@ def get(self, step=False): res, body = self.protocol_handler.getresponse() if res.status == 404: - self.err.set_error(self.err.NOTFOUND) return None, None if res.status != 200: - self.err.set_error(self.err.EMISC) - return False, False + raise KyotoTycoonException('protocol error [%d]' % res.status) res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) key = res_dict[b'key'].decode('utf-8') value = self.unpack(res_dict[b'value']) - self.err.set_success() return key, value def seize(self): @@ -293,14 +281,12 @@ def seize(self): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) seize_dict = {'key': res_dict[b'key'].decode('utf-8'), 'value': self.unpack(res_dict[b'value'])} - self.err.set_success() return seize_dict def delete(self): @@ -313,16 +299,13 @@ def delete(self): res, body = self.protocol_handler.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True class ProtocolHandler(object): - def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None, exceptions=False): - self.err = KyotoTycoonError(exceptions) + def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): self.pack_type = pack_type if pack_type != KT_PACKER_CUSTOM and custom_packer is not None: @@ -390,10 +373,8 @@ def echo(self): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def get(self, key, db=0): @@ -404,14 +385,11 @@ def get(self, key, db=0): res, body = self.getresponse() if res.status == 404: - self.err.set_error(self.err.NOTFOUND) return None if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return self.unpack(body) def set_bulk(self, kv_dict, expire, atomic, db=0): @@ -432,10 +410,9 @@ def set_bulk(self, kv_dict, expire, atomic, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() + # Number of items set... return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def remove_bulk(self, keys, atomic, db=0): @@ -454,16 +431,10 @@ def remove_bulk(self, keys, atomic, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - count = int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) - if count > 0: - self.err.set_success() - else: - self.err.set_error(self.err.NOTFOUND) - - return count + # Number of items removed... + return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def get_bulk(self, keys, atomic, db=0): if len(keys) < 1: @@ -481,22 +452,19 @@ def get_bulk(self, keys, atomic, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) rv = {} res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) n = res_dict.pop(b'num') if n == '0': - self.err.set_error(self.err.NOTFOUND) return {} for k, v in res_dict.items(): if v is not None: rv[k.decode('utf-8')[1:]] = self.unpack(v) - self.err.set_success() return rv def get_int(self, key, db=0): @@ -507,10 +475,8 @@ def get_int(self, key, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.NOTFOUND) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return struct.unpack('>q', body)[0] def vacuum(self, db=0): @@ -521,16 +487,13 @@ def vacuum(self, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def match_prefix(self, prefix, limit, db=0): if prefix is None: - self.err.set_error(self.err.LOGIC, 'no key prefix specified') - return None + raise ValueError('no key prefix specified') db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path = '/rpc/match_prefix?DB=' + db @@ -544,29 +507,25 @@ def match_prefix(self, prefix, limit, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) rv = [] res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) if len(res_list) == 0 or res_list[-1][0] != b'num': - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('server returned no data') + num_key, num = res_list.pop() if num == '0': - self.err.set_error(self.err.NOTFOUND) return [] for k, v in res_list: rv.append(k.decode('utf-8')[1:]) - self.err.set_success() return rv def match_regex(self, regex, limit, db=0): if regex is None: - self.err.set_error(self.err.LOGIC, 'no regular expression specified') - return None + raise ValueError('no regular expression specified') db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path = '/rpc/match_regex?DB=' + db @@ -580,23 +539,20 @@ def match_regex(self, regex, limit, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) rv = [] res_list = _tsv_to_list(body, res.getheader('Content-Type', '')) if len(res_list) == 0 or res_list[-1][0] != b'num': - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('server returned no data') + num_key, num = res_list.pop() if num == '0': - self.err.set_error(self.err.NOTFOUND) return [] for k, v in res_list: rv.append(k.decode('utf-8')[1:]) - self.err.set_success() return rv def set(self, key, value, expire, db=0): @@ -606,10 +562,8 @@ def set(self, key, value, expire, db=0): value = self.pack(value) status = self._rest_put('set', path, value, expire) if status != 201: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % status) - self.err.set_success() return True def add(self, key, value, expire, db=0): @@ -619,16 +573,13 @@ def add(self, key, value, expire, db=0): value = self.pack(value) status = self._rest_put('add', path, value, expire) if status != 201: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % status) - self.err.set_success() return True def cas(self, key, old_val, new_val, expire, db=0): if old_val is None and new_val is None: - self.err.set_error(self.err.LOGIC, 'old value and/or new value must be specified') - return False + raise ValueError('old value and/or new value must be specified') db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path = '/rpc/cas?DB=' + db @@ -650,10 +601,8 @@ def cas(self, key, old_val, new_val, expire, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def remove(self, key, db=0): @@ -664,10 +613,8 @@ def remove(self, key, db=0): res, body = self.getresponse() if res.status != 204: - self.err.set_error(self.err.NOTFOUND) return False - self.err.set_success() return True def replace(self, key, value, expire, db=0): @@ -677,10 +624,8 @@ def replace(self, key, value, expire, db=0): value = self.pack(value) status = self._rest_put('replace', path, value, expire) if status != 201: - self.err.set_error(self.err.NOTFOUND) return False - self.err.set_success() return True def append(self, key, value, expire, db=0): @@ -701,8 +646,7 @@ def append(self, key, value, expire, db=0): if (not isinstance(data, bytes_type) and not isinstance(data, unicode_type)): - self.err.set_error(self.err.EMISC, 'stored value is not a string or bytes type') - return False + raise KyotoTycoonException('stored value is not a string or bytes type') if type(data) != type(value): if isinstance(data, bytes_type): @@ -714,10 +658,8 @@ def append(self, key, value, expire, db=0): # This makes the operation atomic... if self.cas(key, old_data, data, expire, db) is not True: - self.err.set_error(self.err.EMISC, 'error while storing modified value') - return False + raise KyotoTycoonException('error while storing modified value') - self.err.set_success() return True def increment(self, key, delta, expire, db=0): @@ -729,16 +671,13 @@ def increment(self, key, delta, expire, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return int(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def increment_double(self, key, delta, expire, db=0): if key is None: - self.err.set_error(self.err.LOGIC, 'no key specified') - return False + raise ValueError('no key specified') db = str(db) if isinstance(db, int) else quote(db.encode('utf-8')) path = '/rpc/increment_double?DB=' + db @@ -748,25 +687,21 @@ def increment_double(self, key, delta, expire, db=0): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return float(_tsv_to_dict(body, res.getheader('Content-Type', ''))[b'num']) def report(self): self.conn.request('GET', '/rpc/report') res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) report_dict = {} for k, v in res_dict.items(): report_dict[k.decode('utf-8')] = v.decode('utf-8') - self.err.set_success() return report_dict def status(self, db=0): @@ -776,15 +711,13 @@ def status(self, db=0): self.conn.request('GET', path) res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) status_dict = {} for k, v in res_dict.items(): status_dict[k.decode('utf-8')] = v.decode('utf-8') - self.err.set_success() return status_dict def clear(self, db=0): @@ -794,10 +727,8 @@ def clear(self, db=0): self.conn.request('GET', path) res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.EMISC) - return False + raise KyotoTycoonException('protocol error [%d]' % res.status) - self.err.set_success() return True def count(self, db=0): @@ -827,8 +758,7 @@ def play_script(self, name, kv_dict=None): res, body = self.getresponse() if res.status != 200: - self.err.set_error(self.err.MISC) - return None + raise KyotoTycoonException('protocol error [%d]' % res.status) rv = {} res_dict = _tsv_to_dict(body, res.getheader('Content-Type', '')) @@ -837,7 +767,6 @@ def play_script(self, name, kv_dict=None): if v is not None: rv[k.decode('utf-8')[1:]] = v - self.err.set_success() return rv def _rest_put(self, operation, key, value, expire): diff --git a/kyototycoon/kyototycoon.py b/kyototycoon/kyototycoon.py index 88da5fa..3a52ee3 100644 --- a/kyototycoon/kyototycoon.py +++ b/kyototycoon/kyototycoon.py @@ -8,37 +8,35 @@ # Note that python-kyototycoon follows the following interface # standard: http://fallabs.com/kyototycoon/kyototycoon.idl +import warnings + from . import kt_http from . import kt_binary from .kt_common import KT_PACKER_PICKLE -KT_DEFAULT_HOST = '127.0.0.1' -KT_DEFAULT_PORT = 1978 -KT_DEFAULT_TIMEOUT = 30 - class KyotoTycoon(object): def __init__(self, binary=False, pack_type=KT_PACKER_PICKLE, - custom_packer=None, exceptions=False): + custom_packer=None, exceptions=True): ''' Initialize a "Binary Protocol" or "HTTP Protocol" KyotoTycoon object. - The library doesn't raise exceptions for server errors (soft errors) by - default (for historical reasons). This behavior can be changed by using - the "exceptions" parameter. - Note: The default packer uses pickle protocol v2, which is the highest version that's still compatible with both Python 2 and 3. If you require a different version, specify a custom packer object. ''' + if not exceptions: + # Relying on separate error states is bad form and should be avoided... + raise DeprecationWarning('not raising exceptions on error has been removed') + if binary: self.atomic = False # The binary protocol does not support atomic operations. - self.core = kt_binary.ProtocolHandler(pack_type, custom_packer, exceptions) + self.core = kt_binary.ProtocolHandler(pack_type, custom_packer) else: self.atomic = True - self.core = kt_http.ProtocolHandler(pack_type, custom_packer, exceptions) + self.core = kt_http.ProtocolHandler(pack_type, custom_packer) def __enter__(self): return self @@ -46,12 +44,7 @@ def __enter__(self): def __exit__(self, type, value, traceback): self.close() - def error(self): - '''Return the error state from the last operation.''' - - return self.core.error() - - def open(self, host=KT_DEFAULT_HOST, port=KT_DEFAULT_PORT, timeout=KT_DEFAULT_TIMEOUT): + def open(self, host='127.0.0.1', port=1978, timeout=30): '''Open a new connection to a KT server.''' return True if self.core.open(host, port, timeout) else False diff --git a/tests/t_arithmetic.py b/tests/t_arithmetic.py index 26d16e1..0b85c4c 100644 --- a/tests/t_arithmetic.py +++ b/tests/t_arithmetic.py @@ -7,7 +7,7 @@ import config import unittest -from kyototycoon import KyotoTycoon +from kyototycoon import KyotoTycoon, KyotoTycoonException class UnitTest(unittest.TestCase): def setUp(self): @@ -27,8 +27,7 @@ def test_increment(self): # Incrementing against a non numeric value. Must fail. self.assertTrue(self.kt_handle.set(key, 'foo')) - self.assertEqual(self.kt_handle.increment(key, 10), None) - self.assertEqual(self.kt_handle.increment(key, 10), None) + self.assertRaises(KyotoTycoonException, self.kt_handle.increment, key, 10) def test_increment_double(self): self.assertTrue(self.kt_handle.clear()) diff --git a/tests/t_multi.py b/tests/t_multi.py index 17dad88..bfda8ca 100644 --- a/tests/t_multi.py +++ b/tests/t_multi.py @@ -11,7 +11,7 @@ import config import time import unittest -from kyototycoon import KyotoTycoon +from kyototycoon import KyotoTycoon, KyotoTycoonException DB_1 = 0 DB_2 = 1 @@ -39,20 +39,17 @@ def test_status(self): status = self.kt_handle_http.status(DB_2) assert status is not None - status = self.kt_handle_http.status(DB_INVALID) - assert status is None - - status = self.kt_handle_http.status('non_existent') - assert status is None + self.assertRaises(KyotoTycoonException, self.kt_handle_http.status, DB_INVALID) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.status, 'non_existent') def test_set_get(self): self.assertTrue(self.clear_all()) self.assertTrue(self.kt_handle_http.set('ice', 'cream', db=DB_2)) - self.assertFalse(self.kt_handle_http.set('palo', 'alto', db=DB_INVALID)) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.set, 'palo', 'alto', db=DB_INVALID) self.assertEqual(self.kt_handle_http.get('ice'), None) self.assertEqual(self.kt_handle_http.get('ice', db=DB_1), None) - self.assertFalse(self.kt_handle_http.get('ice', db=DB_INVALID)) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.get, 'ice', db=DB_INVALID) self.assertEqual(self.kt_handle_http.get('ice', db=DB_2), 'cream') self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) @@ -64,7 +61,7 @@ def test_set_get(self): self.assertEqual(self.kt_handle_http.get('frozen'), 'yoghurt') self.assertEqual(self.kt_handle_http.get('frozen', db=DB_1), 'yoghurt') self.assertEqual(self.kt_handle_http.get('frozen', db=DB_2), None) - self.assertFalse(self.kt_handle_http.get('frozen', db=DB_INVALID), None) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.get, 'frozen', db=DB_INVALID) self.assertTrue(self.kt_handle_http.clear(db=DB_1)) self.assertEqual(self.kt_handle_http.count(db=DB_1), 0) @@ -104,9 +101,9 @@ def test_add(self): self.assertTrue(self.kt_handle_http.add('key1', 'val1', db=DB_2)) # Now they should. - self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_1)) - self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_2)) - self.assertFalse(self.kt_handle_http.add('key1', 'val1', db=DB_INVALID)) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.add, 'key1', 'val1', db=DB_1) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.add, 'key1', 'val1', db=DB_2) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.add, 'key1', 'val1', db=DB_INVALID) def test_replace(self): self.assertTrue(self.clear_all()) @@ -125,7 +122,7 @@ def test_cas(self): self.assertTrue(self.clear_all()) self.assertTrue(self.kt_handle_http.set('key', 'xxx')) self.assertEqual(self.kt_handle_http.get('key', db=DB_2), None) - self.assertFalse(self.kt_handle_http.cas('key', old_val='xxx', new_val='yyy', db=DB_2)) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.cas, 'key', old_val='xxx', new_val='yyy', db=DB_2) self.assertEqual(self.kt_handle_http.get('key', db=DB_1), 'xxx') self.assertTrue(self.kt_handle_http.cas('key', old_val='xxx', new_val='yyy', db=DB_1)) self.assertTrue(self.kt_handle_http.cas('key', new_val='xxx', db=DB_2)) @@ -143,7 +140,7 @@ def test_vacuum(self): self.assertTrue(self.kt_handle_http.vacuum()) self.assertTrue(self.kt_handle_http.vacuum(db=DB_1)) self.assertTrue(self.kt_handle_http.vacuum(db=DB_2)) - self.assertFalse(self.kt_handle_http.vacuum(db=DB_INVALID)) + self.assertRaises(KyotoTycoonException, self.kt_handle_http.vacuum, db=DB_INVALID) def test_append(self): self.assertTrue(self.clear_all()) diff --git a/tests/t_simple.py b/tests/t_simple.py index d3dbc91..ad3140c 100644 --- a/tests/t_simple.py +++ b/tests/t_simple.py @@ -7,8 +7,7 @@ import config import unittest -from kyototycoon import KyotoTycoon -from kyototycoon.kt_http import KT_PACKER_PICKLE +from kyototycoon import KyotoTycoon, KyotoTycoonException, KT_PACKER_PICKLE class UnitTest(unittest.TestCase): def setUp(self): @@ -18,10 +17,8 @@ def setUp(self): def test_set(self): self.assertTrue(self.kt_handle.clear()) - error = self.kt_handle.error() self.assertTrue(self.kt_handle.set('key', 'value')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.set('k e y', 'v a l u e')) self.assertTrue(self.kt_handle.set('k\te\ty', 'tabbed')) @@ -46,74 +43,50 @@ def test_set(self): self.assertTrue(self.kt_handle.set('https://github.com/blog/', 'url3')) self.assertEqual(self.kt_handle.get('non_existent'), None) - self.assertEqual(error.code(), error.NOTFOUND) self.assertTrue(self.kt_handle.set('cb', 1791)) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('cb'), 1791) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.set('cb', 1791.1226)) self.assertEqual(self.kt_handle.get('cb'), 1791.1226) def test_cas(self): self.assertTrue(self.kt_handle.clear()) - error = self.kt_handle.error() self.assertTrue(self.kt_handle.set('key', 'xxx')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.cas('key', old_val='xxx', new_val='yyy')) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('key'), 'yyy') - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.cas('key', old_val='yyy')) assert self.kt_handle.get('key') is None - self.assertEqual(error.code(), error.NOTFOUND) self.assertTrue(self.kt_handle.cas('key', new_val='zzz')) self.assertEqual(self.kt_handle.get('key'), 'zzz') - self.assertEqual(error.code(), error.SUCCESS) - self.assertFalse(self.kt_handle.cas('key', old_val='foo', new_val='zz')) - self.assertEqual(error.code(), error.EMISC) + self.assertRaises(KyotoTycoonException, self.kt_handle.cas, 'key', old_val='foo', new_val='zz') self.assertEqual(self.kt_handle.get('key'), 'zzz') - self.assertEqual(error.code(), error.SUCCESS) def test_remove(self): self.assertTrue(self.kt_handle.clear()) - error = self.kt_handle.error() self.assertFalse(self.kt_handle.remove('must fail key')) - self.assertEqual(error.code(), error.NOTFOUND) self.assertTrue(self.kt_handle.set('deleteable key', 'xxx')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.remove('deleteable key')) - self.assertEqual(error.code(), error.SUCCESS) def test_replace(self): self.assertTrue(self.kt_handle.clear()) - error = self.kt_handle.error() # Must Fail - Can't replace something that doesn't exist. self.assertFalse(self.kt_handle.replace('xxxxxx', 'some value')) - self.assertEqual(error.code(), error.NOTFOUND) # Popuate then Replace. self.assertTrue(self.kt_handle.set('apple', 'ringo')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.replace('apple', 'apfel')) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('apple'), 'apfel') - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.replace('apple', 212)) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('apple'), 212) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.replace('apple', 121)) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('apple'), 121) - self.assertEqual(error.code(), error.SUCCESS) def test_append(self): self.assertTrue(self.kt_handle.clear()) @@ -139,7 +112,7 @@ def test_add(self): self.assertTrue(self.kt_handle.set('stewie', 'griffin')) # Must Fail - Stewie exists - self.assertFalse(self.kt_handle.add('stewie', 'hopkin')) + self.assertRaises(KyotoTycoonException, self.kt_handle.add, 'stewie', 'hopkin') # New records self.assertTrue(self.kt_handle.add('peter', 'griffin')) @@ -232,12 +205,6 @@ def test_status(self): self.kt_handle.set('pink', 'peach') self.assertTrue(status['count'], 3) - def test_error(self): - self.assertTrue(self.kt_handle.clear()) - kt_error = self.kt_handle.error() - assert kt_error is not None - self.assertEqual(kt_error.code(), kt_error.SUCCESS) - def test_vacuum(self): self.assertTrue(self.kt_handle.vacuum()) diff --git a/tests/t_simple_binary.py b/tests/t_simple_binary.py index 4753d40..2c5b367 100644 --- a/tests/t_simple_binary.py +++ b/tests/t_simple_binary.py @@ -22,10 +22,8 @@ def setUp(self): def test_set(self): self.assertTrue(self.kt_handle_http.clear()) - error = self.kt_handle.error() self.assertTrue(self.kt_handle.set('key', 'value')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.set('k e y', 'v a l u e')) self.assertTrue(self.kt_handle.set('k\te\ty', 'tabbed')) @@ -50,26 +48,19 @@ def test_set(self): self.assertTrue(self.kt_handle.set('https://github.com/blog/', 'url3')) self.assertEqual(self.kt_handle.get('non_existent'), None) - self.assertEqual(error.code(), error.NOTFOUND) self.assertTrue(self.kt_handle.set('cb', 1791)) - self.assertEqual(error.code(), error.SUCCESS) self.assertEqual(self.kt_handle.get('cb'), 1791) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.set('cb', 1791.1226)) self.assertEqual(self.kt_handle.get('cb'), 1791.1226) def test_remove(self): self.assertTrue(self.kt_handle_http.clear()) - error = self.kt_handle.error() self.assertFalse(self.kt_handle.remove('must fail key')) - self.assertEqual(error.code(), error.NOTFOUND) self.assertTrue(self.kt_handle.set('deleteable key', 'xxx')) - self.assertEqual(error.code(), error.SUCCESS) self.assertTrue(self.kt_handle.remove('deleteable key')) - self.assertEqual(error.code(), error.SUCCESS) def test_set_bulk(self): self.assertTrue(self.kt_handle_http.clear()) @@ -134,12 +125,6 @@ def test_large_key(self): self.assertTrue(self.kt_handle.set(large_key, 'value')) self.assertEqual(self.kt_handle.get(large_key), 'value') - def test_error(self): - self.assertTrue(self.kt_handle_http.clear()) - kt_error = self.kt_handle.error() - assert kt_error is not None - self.assertEqual(kt_error.code(), kt_error.SUCCESS) - if __name__ == '__main__': unittest.main() From 58acf6941f4515b0a1864bafa14fe9267f24e85b Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Wed, 19 Mar 2014 12:21:48 +0000 Subject: [PATCH 61/62] Bump version to reflect exception changes. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c4721b6..4e54804 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer_email='cefrodrigues@gmail.com', name='python-kyototycoon', description='Kyoto Tycoon Client Library', - version='0.5.9', + version='0.6.0', license='BSD', keywords='Kyoto Tycoon, Kyoto Cabinet', packages=['kyototycoon'], From 2866de07ad6c87b094bdb54434788c4a3d33832d Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Thu, 20 Mar 2014 10:42:17 +0000 Subject: [PATCH 62/62] Remove now dangling error method. --- kyototycoon/kt_binary.py | 3 --- kyototycoon/kt_http.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/kyototycoon/kt_binary.py b/kyototycoon/kt_binary.py index 137a709..ed65563 100644 --- a/kyototycoon/kt_binary.py +++ b/kyototycoon/kt_binary.py @@ -69,9 +69,6 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): else: raise KyotoTycoonException('unsupported pack type specified') - def error(self): - return self.err - def cursor(self): raise NotImplementedError('supported under the HTTP procotol only') diff --git a/kyototycoon/kt_http.py b/kyototycoon/kt_http.py index 67750bb..c925b38 100644 --- a/kyototycoon/kt_http.py +++ b/kyototycoon/kt_http.py @@ -338,9 +338,6 @@ def __init__(self, pack_type=KT_PACKER_PICKLE, custom_packer=None): else: raise KyotoTycoonException('unsupported pack type specified') - def error(self): - return self.err - def cursor(self): return Cursor(self)