Skip to content

Commit 2d41d65

Browse files
committed
Initial checkin.
0 parents  commit 2d41d65

File tree

6 files changed

+379
-0
lines changed

6 files changed

+379
-0
lines changed

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2010 Charles Leifer
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include LICENSE
2+
include README.md
3+
include tests.py
4+
recursive-include docs *

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ukt

setup.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
from setuptools import setup
3+
4+
5+
with open(os.path.join(os.path.dirname(__file__), 'README.md')) as fh:
6+
readme = fh.read()
7+
8+
9+
setup(
10+
name='ukt',
11+
version=__import__('ukt').__version__,
12+
description='lightweight kyototycoon client',
13+
long_description=readme,
14+
author='Charles Leifer',
15+
author_email='[email protected]',
16+
url='http://github.com/coleifer/ukt/',
17+
packages=[],
18+
py_modules=['ukt'],
19+
test_suite='tests')

tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
import unittest
5+
6+
from ukt import *
7+
8+
9+
class BaseTestCache(object):
10+
pass
11+
12+
13+
if __name__ == '__main__':
14+
unittest.main(argv=sys.argv)

ukt.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
from contextlib import contextmanager
2+
import heapq
3+
import io
4+
import socket
5+
import struct
6+
import time
7+
8+
9+
class KTError(Exception): pass
10+
class ProtocolError(KTError): pass
11+
class ServerConnectionError(KTError): pass
12+
class ServerError(KTError): pass
13+
14+
15+
SET_BULK = b'\xb8'
16+
GET_BULK = b'\xba'
17+
REMOVE_BULK = b'\xb9'
18+
PLAY_SCRIPT = b'\xb4'
19+
ERROR = b'\xbf'
20+
NO_REPLY = 0x01
21+
EXPIRE = 0x7fffffffffffffff
22+
23+
24+
def encode(s):
25+
if isinstance(s, str):
26+
return s.encode('utf8')
27+
elif isinstance(s, bytes):
28+
return s
29+
elif s is not None:
30+
return str(s).encode('utf8')
31+
32+
def decode(s):
33+
if isinstance(s, bytes):
34+
return s.decode('utf8')
35+
elif isinstance(s, str):
36+
return s
37+
elif s is not None:
38+
return str(s)
39+
40+
41+
READSIZE = 1024 * 4
42+
43+
44+
class Socket(object):
45+
def __init__(self, s):
46+
self.sock = s
47+
self.is_closed = False
48+
self.buf = io.BytesIO()
49+
self.bytes_read = self.bytes_written = 0
50+
self.recvbuf = bytearray(READSIZE)
51+
52+
def __del__(self):
53+
if not self.is_closed:
54+
self.sock.close()
55+
56+
def _read_from_socket(self, length):
57+
l = marker = 0
58+
recvptr = memoryview(self.recvbuf)
59+
self.buf.seek(self.bytes_written)
60+
61+
try:
62+
while True:
63+
l = self.sock.recv_into(recvptr, READSIZE)
64+
if not l:
65+
self.close()
66+
raise ServerConnectionError('server went away')
67+
self.buf.write(recvptr[:l])
68+
self.bytes_written += l
69+
marker += l
70+
if length > 0 and length > marker:
71+
continue
72+
break
73+
except socket.timeout:
74+
raise ServerConnectionError('timed out reading from socket')
75+
except socket.error:
76+
raise ServerConnectionError('error reading from socket')
77+
78+
def recv(self, length):
79+
buflen = self.bytes_written - self.bytes_read
80+
if length > buflen:
81+
self._read_from_socket(length - buflen)
82+
83+
self.buf.seek(self.bytes_read)
84+
data = self.buf.read(length)
85+
self.bytes_read += length
86+
87+
if self.bytes_read == self.bytes_written:
88+
self.purge()
89+
return data
90+
91+
def send(self, data):
92+
try:
93+
self.sock.sendall(data)
94+
except IOError:
95+
self.close()
96+
raise ServerConnectionError('server went away')
97+
98+
def purge(self):
99+
self.buf.seek(0)
100+
self.buf.truncate()
101+
self.bytes_read = self.bytes_written = 0
102+
103+
def close(self):
104+
if self.is_closed:
105+
return False
106+
107+
self.is_closed = True
108+
try:
109+
self.sock.shutdown(socket.SHUT_RDWR)
110+
except OSError:
111+
pass
112+
self.sock.close()
113+
114+
self.purge()
115+
self.buf.close()
116+
self.buf = None
117+
return True
118+
119+
120+
class Pool(object):
121+
def __init__(self, host, port, timeout=None, max_age=3600):
122+
self.host = host
123+
self.port = port
124+
self.timeout = timeout
125+
self.max_age = max_age or 3600
126+
self.in_use = set()
127+
self.free = []
128+
129+
def create_socket(self):
130+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
131+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
132+
if self.timeout:
133+
sock.settimeout(self.timeout)
134+
sock.connect((self.host, self.port))
135+
return Socket(sock)
136+
137+
def checkout(self):
138+
threshold = time.time() - self.max_age
139+
while self.free:
140+
ts, sock = heapq.heappop(self.free)
141+
if ts > threshold:
142+
self.in_use.add(sock)
143+
return sock
144+
else:
145+
sock.close()
146+
147+
sock = self.create_socket()
148+
self.in_use.add(sock)
149+
return sock
150+
151+
def checkin(self, sock):
152+
self.in_use.remove(sock)
153+
if not sock.is_closed:
154+
heapq.heappush(self.free, (time.time(), sock))
155+
156+
def close(self):
157+
n = 0
158+
while self.free:
159+
_, sock = self.free.pop()
160+
sock.close()
161+
n += 1
162+
163+
tmp, self.in_use = self.in_use, set()
164+
for sock in tmp:
165+
sock.close()
166+
n += 1
167+
168+
return n
169+
170+
171+
class Ctx(object):
172+
__slots__ = ('pool', 'sock')
173+
174+
def __init__(self, pool):
175+
self.pool = pool
176+
self.sock = None
177+
178+
def __enter__(self):
179+
self.sock = self.pool.checkout()
180+
return self.sock
181+
182+
def __exit__(self, exc_type, exc_val, exc_tb):
183+
self.pool.checkin(self.sock)
184+
185+
186+
class Protocol(object):
187+
def __init__(self, host='127.0.0.1', port=1978, decode_keys=True,
188+
encode_value=None, decode_value=None, timeout=None,
189+
max_age=3600):
190+
self.pool = Pool(host, port, timeout, max_age)
191+
self.decode_keys = decode_keys
192+
self.encode_value = encode_value or encode
193+
self.decode_value = decode_value or decode
194+
195+
@contextmanager
196+
def ctx(self):
197+
sock = self.pool.checkout()
198+
try:
199+
yield sock
200+
except socket.error:
201+
sock.close()
202+
finally:
203+
self.pool.checkin(sock)
204+
205+
def get_bulk(self, keys, db=None, decode_values=True):
206+
"""
207+
Get multiple key/value pairs in a single request.
208+
209+
:param keys: a list of keys.
210+
:param int db: database index.
211+
:param bool decode_values: deserialize values after reading.
212+
:return: a dict of key, value for matching records.
213+
"""
214+
with self.ctx() as sock:
215+
pass
216+
217+
def get_bulk_details(self, db_key_list, decode_values=True):
218+
"""
219+
Get all data for a given list of db, key pairs.
220+
221+
:param db_key_list: a list of (db, key) tuples.
222+
:param bool decode_values: deserialize values after reading.
223+
:return: a list of (db, key, value, xt) tuples.
224+
"""
225+
with self.ctx() as sock:
226+
pass
227+
228+
def get(self, key, db=None, decode_value=True):
229+
"""
230+
Get the value for a given key.
231+
232+
:param key: key to fetch.
233+
:param int db: database index.
234+
:param bool decode_value: deserialize value after reading.
235+
:return: value or None.
236+
"""
237+
db_key_list = ((db, key),)
238+
result = self.get_bulk_details(db_key_list, decode_value)
239+
if result:
240+
return result[0][2]
241+
242+
def set_bulk(self, data, db=None, expire_time=None, no_reply=False,
243+
encode_values=True):
244+
"""
245+
Set multiple key/value pairs in a single request.
246+
247+
:param dict data: a mapping of key to value.
248+
:param int db: database index.
249+
:param long expire_time: expire time in seconds from now.
250+
:param bool no_reply: do not receive a response.
251+
:param bool encode_values: serialize values before writing.
252+
:return: number of records written.
253+
"""
254+
with self.ctx() as sock:
255+
pass
256+
257+
def set_bulk_details(self, data, no_reply=False, encode_values=True):
258+
"""
259+
Set multiple key/value pairs in a single request, optionally across
260+
multiple databases with varying expire time(s).
261+
262+
:param list data: a list of (db, key, value, xt) tuples.
263+
:param bool no_reply: do not receive a response.
264+
:param bool encode_values: serialize values before writing.
265+
:return: number of records written.
266+
"""
267+
with self.ctx() as sock:
268+
pass
269+
270+
def set(self, key, value, db=None, expire_time=None, no_reply=False,
271+
encode_value=True):
272+
return self.set_bulk({key: value}, db, expire_time, no_reply,
273+
encode_value)
274+
275+
def remove_bulk(self, keys, db=None, no_reply=False):
276+
"""
277+
Remove multiple key/value pairs in a single request.
278+
279+
:param keys: a list of keys.
280+
:param int db: database index.
281+
:param bool no_reply: do not receive a response.
282+
:return: number of records removed.
283+
"""
284+
with self.ctx() as sock:
285+
pass
286+
287+
def remove_bulk_details(self, db_key_list, no_reply=False):
288+
"""
289+
Get all data for a given list of db, key pairs.
290+
291+
:param db_key_list: a list of (db, key) tuples.
292+
:param bool no_reply: do not receive a response.
293+
:return: number of records removed.
294+
"""
295+
with self.ctx() as sock:
296+
pass
297+
298+
def remove(self, key, db=None, no_reply=False):
299+
"""
300+
Remove a single key from the database.
301+
302+
:param key: key to remove.
303+
:param int db: database index.
304+
:param bool no_reply: do not receive a response.
305+
:return: number of records removed.
306+
"""
307+
return self.remove_bulk((key,), db, no_reply)
308+
309+
def script(self, name, data=None, no_reply=False, encode_values=True,
310+
decode_values=True):
311+
"""
312+
Evaluate a lua script.
313+
314+
:param name: script function name.
315+
:param dict data: dictionary of key/value pairs, passed as arguments.
316+
:param bool no_reply: do not receive a response.
317+
:param bool encode_values: serialize values before sending to db.
318+
:param bool decode_values: deserialize values after reading result.
319+
:return: dictionary of key/value pairs returned by the lua function.
320+
"""
321+
with self.ctx() as sock:
322+
pass

0 commit comments

Comments
 (0)