Merge branch 'client-feature' into develop

This commit is contained in:
siddontang 2014-07-09 09:44:28 +08:00
commit 961b4f393d
22 changed files with 3846 additions and 1 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
build
*.pyc
.DS_Store

51
client/ledis-py/README.md Normal file
View File

@ -0,0 +1,51 @@
#ledis-py
The Python interface to the ledisdb key-value store.
##Installation
ledis-py requires a running ledisdb server. See [ledisdb guide](https://github.com/siddontang/ledisdb#build-and-install) for installation instructions.
To install ledis-py, simply using `pip`(recommended):
```
$ sudo pip install ledis
```
or alternatively, using `easy_install`:
```
$ sudo easy_install ledis
```
or install from the source:
```
$ sudo python setup.py install
```
##Getting Started
```
>>> import ledis
>>> l = ledis.Ledis(host='localhost', port=6380, db=0)
>>> l.set('foo', 'bar')
True
>>> l.get('foo')
'bar'
>>>
```
## API Reference
For full API reference, please visit [rtfd](http://ledis-py.readthedocs.org/).
## Connection
### Connection Pools
### Connnections

View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ledis-py.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ledis-py.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/ledis-py"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ledis-py"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
#
# ledis-py documentation build configuration file, created by
# sphinx-quickstart on Tue Jul 8 20:03:17 2014.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('../..'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'ledis-py'
copyright = u'2014, Andy McCurdy'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.0.1'
# The full version, including alpha/beta/rc tags.
release = '0.0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'ledis-pydoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'ledis-py.tex', u'ledis-py Documentation',
u'Andy McCurdy', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'ledis-py', u'ledis-py Documentation',
[u'Andy McCurdy'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'ledis-py', u'ledis-py Documentation',
u'Andy McCurdy', 'ledis-py', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -0,0 +1,26 @@
.. ledis-py documentation master file, created by
sphinx-quickstart on Tue Jul 8 20:03:17 2014.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
ledis-py's documentation!
====================================
API Reference
-------------
.. toctree::
:maxdepth: 2
.. automodule:: ledis
:members:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,27 @@
from ledis.client import Ledis
from ledis.connection import (
BlockingConnectionPool,
ConnectionPool,
Connection,
UnixDomainSocketConnection
)
from ledis.utils import from_url
from ledis.exceptions import (
ConnectionError,
BusyLoadingError,
DataError,
InvalidResponse,
LedisError,
ResponseError,
)
__version__ = '0.0.1'
VERSION = tuple(map(int, __version__.split('.')))
__all__ = [
'Ledis', 'ConnectionPool', 'BlockingConnectionPool',
'Connection', 'UnixDomainSocketConnection',
'LedisError', 'ConnectionError', 'ResponseError',
'InvalidResponse', 'DataError', 'from_url', 'BusyLoadingError',
]

View File

@ -0,0 +1,79 @@
"""Internal module for Python 2 backwards compatibility."""
import sys
if sys.version_info[0] < 3:
from urlparse import parse_qs, urlparse
from itertools import imap, izip
from string import letters as ascii_letters
from Queue import Queue
try:
from cStringIO import StringIO as BytesIO
except ImportError:
from StringIO import StringIO as BytesIO
iteritems = lambda x: x.iteritems()
iterkeys = lambda x: x.iterkeys()
itervalues = lambda x: x.itervalues()
nativestr = lambda x: \
x if isinstance(x, str) else x.encode('utf-8', 'replace')
u = lambda x: x.decode()
b = lambda x: x
next = lambda x: x.next()
byte_to_chr = lambda x: x
unichr = unichr
xrange = xrange
basestring = basestring
unicode = unicode
bytes = str
long = long
else:
from urllib.parse import parse_qs, urlparse
from io import BytesIO
from string import ascii_letters
from queue import Queue
iteritems = lambda x: iter(x.items())
iterkeys = lambda x: iter(x.keys())
itervalues = lambda x: iter(x.values())
byte_to_chr = lambda x: chr(x)
nativestr = lambda x: \
x if isinstance(x, str) else x.decode('utf-8', 'replace')
u = lambda x: x
b = lambda x: x.encode('iso-8859-1') if not isinstance(x, bytes) else x
next = next
unichr = chr
imap = map
izip = zip
xrange = range
basestring = str
unicode = str
bytes = bytes
long = int
try: # Python 3
from queue import LifoQueue, Empty, Full
except ImportError:
from Queue import Empty, Full
try: # Python 2.6 - 2.7
from Queue import LifoQueue
except ImportError: # Python 2.5
from Queue import Queue
# From the Python 2.7 lib. Python 2.5 already extracted the core
# methods to aid implementating different queue organisations.
class LifoQueue(Queue):
"Override queue methods to implement a last-in first-out queue."
def _init(self, maxsize):
self.maxsize = maxsize
self.queue = []
def _qsize(self, len=len):
return len(self.queue)
def _put(self, item):
self.queue.append(item)
def _get(self):
return self.queue.pop()

View File

@ -0,0 +1,705 @@
from __future__ import with_statement
import datetime
import time as mod_time
from ledis._compat import (b, izip, imap, iteritems, iterkeys, itervalues,
basestring, long, nativestr, urlparse, bytes)
from ledis.connection import ConnectionPool, UnixDomainSocketConnection
from ledis.exceptions import (
ConnectionError,
DataError,
LedisError,
ResponseError,
ExecAbortError,
)
SYM_EMPTY = b('')
def list_or_args(keys, args):
# returns a single list combining keys and args
try:
iter(keys)
# a string or bytes instance can be iterated, but indicates
# keys wasn't passed as a list
if isinstance(keys, (basestring, bytes)):
keys = [keys]
except TypeError:
keys = [keys]
if args:
keys.extend(args)
return keys
def string_keys_to_dict(key_string, callback):
return dict.fromkeys(key_string.split(), callback)
def dict_merge(*dicts):
merged = {}
[merged.update(d) for d in dicts]
return merged
def pairs_to_dict(response):
"Create a dict given a list of key/value pairs"
it = iter(response)
return dict(izip(it, it))
def zset_score_pairs(response, **options):
"""
If ``withscores`` is specified in the options, return the response as
a list of (value, score) pairs
"""
if not response or not options['withscores']:
return response
score_cast_func = options.get('score_cast_func', int)
it = iter(response)
return list(izip(it, imap(score_cast_func, it)))
def int_or_none(response):
if response is None:
return None
return int(response)
class Ledis(object):
"""
Implementation of the Redis protocol.
This abstract class provides a Python interface to all LedisDB commands
and an implementation of the Redis protocol.
Connection and Pipeline derive from this, implementing how
the commands are sent and received to the Ledis server
"""
RESPONSE_CALLBACKS = dict_merge(
string_keys_to_dict(
'EXISTS EXPIRE EXPIREAT HEXISTS HMSET SETNX '
'PERSIST HPERSIST LPERSIST ZPERSIST',
bool
),
string_keys_to_dict(
'DECRBY DEL HDEL HLEN INCRBY LLEN '
'ZADD ZCARD ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE'
'LMCLEAR HMCLEAR ZMCLEAR',
int
),
string_keys_to_dict(
'LPUSH RPUSH',
lambda r: isinstance(r, long) and r or nativestr(r) == 'OK'
),
string_keys_to_dict(
'MSET SELECT ',
lambda r: nativestr(r) == 'OK'
),
string_keys_to_dict(
'ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE',
zset_score_pairs
),
string_keys_to_dict('ZRANK ZREVRANK ZSCORE ZINCRBY', int_or_none),
{
'HGETALL': lambda r: r and pairs_to_dict(r) or {},
'PING': lambda r: nativestr(r) == 'PONG',
'SET': lambda r: r and nativestr(r) == 'OK', }
)
@classmethod
def from_url(cls, url, db=None, **kwargs):
"""
Return a Ledis client object configured from the given URL.
For example::
redis://[:password]@localhost:6380/0
unix://[:password]@/path/to/socket.sock?db=0
There are several ways to specify a database number. The parse function
will return the first specified option:
1. A ``db`` querystring option, e.g. redis://localhost?db=0
2. If using the redis:// scheme, the path argument of the url, e.g.
redis://localhost/0
3. The ``db`` argument to this function.
If none of these options are specified, db=0 is used.
Any additional querystring arguments and keyword arguments will be
passed along to the ConnectionPool class's initializer. In the case
of conflicting arguments, querystring arguments always win.
"""
connection_pool = ConnectionPool.from_url(url, db=db, **kwargs)
return cls(connection_pool=connection_pool)
def __init__(self, host='localhost', port=6380,
db=0, socket_timeout=None,
connection_pool=None, charset='utf-8',
errors='strict', decode_responses=False,
unix_socket_path=None):
if not connection_pool:
kwargs = {
'db': db,
'socket_timeout': socket_timeout,
'encoding': charset,
'encoding_errors': errors,
'decode_responses': decode_responses,
}
# based on input, setup appropriate connection args
if unix_socket_path:
kwargs.update({
'path': unix_socket_path,
'connection_class': UnixDomainSocketConnection
})
else:
kwargs.update({
'host': host,
'port': port
})
connection_pool = ConnectionPool(**kwargs)
self.connection_pool = connection_pool
self.response_callbacks = self.__class__.RESPONSE_CALLBACKS.copy()
def set_response_callback(self, command, callback):
"Set a custom Response Callback"
self.response_callbacks[command] = callback
#### COMMAND EXECUTION AND PROTOCOL PARSING ####
def execute_command(self, *args, **options):
"Execute a command and return a parsed response"
pool = self.connection_pool
command_name = args[0]
connection = pool.get_connection(command_name, **options)
try:
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
except ConnectionError:
connection.disconnect()
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
finally:
pool.release(connection)
def parse_response(self, connection, command_name, **options):
"Parses a response from the Ledis server"
response = connection.read_response()
if command_name in self.response_callbacks:
return self.response_callbacks[command_name](response, **options)
return response
#### SERVER INFORMATION ####
def echo(self, value):
"Echo the string back from the server"
return self.execute_command('ECHO', value)
def ping(self):
"Ping the Ledis server"
return self.execute_command('PING')
def select(self, db):
"""Select a Ledis db, ``db`` is integer type"""
try:
db = int(db)
except ValueError:
db = 0
return self.execute_command('SELECT', db)
#### BASIC KEY COMMANDS ####
def decr(self, name, amount=1):
"""
Decrements the value of ``key`` by ``amount``. If no key exists,
the value will be initialized as 0 - ``amount``
"""
return self.execute_command('DECRBY', name, amount)
def decrby(self, name, amount=1):
"""
Decrements the value of ``key`` by ``amount``. If no key exists,
the value will be initialized as 0 - ``amount``
"""
return self.decr(name, amount)
def delete(self, *names):
"Delete one or more keys specified by ``names``"
return self.execute_command('DEL', *names)
def exists(self, name):
"Returns a boolean indicating whether key ``name`` exists"
return self.execute_command('EXISTS', name)
def expire(self, name, time):
"""
Set an expire flag on key ``name`` for ``time`` seconds. ``time``
can be represented by an integer or a Python timedelta object.
"""
if isinstance(time, datetime.timedelta):
time = time.seconds + time.days * 24 * 3600
return self.execute_command('EXPIRE', name, time)
def expireat(self, name, when):
"""
Set an expire flag on key ``name``. ``when`` can be represented
as an integer indicating unix time or a Python datetime object.
"""
if isinstance(when, datetime.datetime):
when = int(mod_time.mktime(when.timetuple()))
return self.execute_command('EXPIREAT', name, when)
def get(self, name):
"""
Return the value at key ``name``, or None if the key doesn't exist
"""
return self.execute_command('GET', name)
def __getitem__(self, name):
"""
Return the value at key ``name``, raises a KeyError if the key
doesn't exist.
"""
value = self.get(name)
if value:
return value
raise KeyError(name)
def getset(self, name, value):
"""
Set the value at key ``name`` to ``value`` if key doesn't exist
Return the value at key ``name`` atomically
"""
return self.execute_command('GETSET', name, value)
def incr(self, name, amount=1):
"""
Increments the value of ``key`` by ``amount``. If no key exists,
the value will be initialized as ``amount``
"""
return self.execute_command('INCRBY', name, amount)
def incrby(self, name, amount=1):
"""
Increments the value of ``key`` by ``amount``. If no key exists,
the value will be initialized as ``amount``
"""
# An alias for ``incr()``, because it is already implemented
# as INCRBY ledis command.
return self.incr(name, amount)
def mget(self, keys, *args):
"""
Returns a list of values ordered identically to ``keys``
"""
args = list_or_args(keys, args)
return self.execute_command('MGET', *args)
def mset(self, *args, **kwargs):
"""
Sets key/values based on a mapping. Mapping can be supplied as a single
dictionary argument or as kwargs.
"""
if args:
if len(args) != 1 or not isinstance(args[0], dict):
raise LedisError('MSET requires **kwargs or a single dict arg')
kwargs.update(args[0])
items = []
for pair in iteritems(kwargs):
items.extend(pair)
return self.execute_command('MSET', *items)
def set(self, name, value):
"""
Set the value of key ``name`` to ``value``.
"""
pieces = [name, value]
return self.execute_command('SET', *pieces)
def setnx(self, name, value):
"Set the value of key ``name`` to ``value`` if key doesn't exist"
return self.execute_command('SETNX', name, value)
def ttl(self, name):
"Returns the number of seconds until the key ``name`` will expire"
return self.execute_command('TTL', name)
def persist(self, name):
"Removes an expiration on name"
return self.execute_command('PERSIST', name)
#### LIST COMMANDS ####
def lindex(self, name, index):
"""
Return the item from list ``name`` at position ``index``
Negative indexes are supported and will return an item at the
end of the list
"""
return self.execute_command('LINDEX', name, index)
def llen(self, name):
"Return the length of the list ``name``"
return self.execute_command('LLEN', name)
def lpop(self, name):
"Remove and return the first item of the list ``name``"
return self.execute_command('LPOP', name)
def lpush(self, name, *values):
"Push ``values`` onto the head of the list ``name``"
return self.execute_command('LPUSH', name, *values)
def lrange(self, name, start, end):
"""
Return a slice of the list ``name`` between
position ``start`` and ``end``
``start`` and ``end`` can be negative numbers just like
Python slicing notation
"""
return self.execute_command('LRANGE', name, start, end)
def rpop(self, name):
"Remove and return the last item of the list ``name``"
return self.execute_command('RPOP', name)
def rpush(self, name, *values):
"Push ``values`` onto the tail of the list ``name``"
return self.execute_command('RPUSH', name, *values)
# SPECIAL COMMANDS SUPPORTED BY LEDISDB
def lclear(self, name):
"Delete the key of ``name``"
return self.execute_command("LCLEAR", name)
def lmclear(self, *names):
"Delete multiple keys of ``name``"
return self.execute_command('LMCLEAR', *names)
def lexpire(self, name, time):
"""
Set an expire flag on key ``name`` for ``time`` seconds. ``time``
can be represented by an integer or a Python timedelta object.
"""
if isinstance(time, datetime.timedelta):
time = time.seconds + time.days * 24 * 3600
return self.execute_command("LEXPIRE", name, time)
def lexpireat(self, name, when):
"""
Set an expire flag on key ``name``. ``when`` can be represented as an integer
indicating unix time or a Python datetime object.
"""
if isinstance(when, datetime.datetime):
when = int(mod_time.mktime(when.timetuple()))
return self.execute_command('LEXPIREAT', name, when)
def lttl(self, name):
"Returns the number of seconds until the key ``name`` will expire"
return self.execute_command('LTTL', name)
def lpersist(self, name):
"Removes an expiration on ``name``"
return self.execute_command('LPERSIST', name)
#### SORTED SET COMMANDS ####
def zadd(self, name, *args, **kwargs):
"""
Set any number of score, element-name pairs to the key ``name``. Pairs
can be specified in two ways:
As *args, in the form of: score1, name1, score2, name2, ...
or as **kwargs, in the form of: name1=score1, name2=score2, ...
The following example would add four values to the 'my-key' key:
ledis.zadd('my-key', 1.1, 'name1', 2.2, 'name2', name3=3.3, name4=4.4)
"""
pieces = []
if args:
if len(args) % 2 != 0:
raise LedisError("ZADD requires an equal number of "
"values and scores")
pieces.extend(args)
for pair in iteritems(kwargs):
pieces.append(pair[1])
pieces.append(pair[0])
return self.execute_command('ZADD', name, *pieces)
def zcard(self, name):
"Return the number of elements in the sorted set ``name``"
return self.execute_command('ZCARD', name)
def zcount(self, name, min, max):
"""
Return the number of elements in the sorted set at key ``name`` with a score
between ``min`` and ``max``.
The min and max arguments have the same semantic as described for ZRANGEBYSCORE.
"""
return self.execute_command('ZCOUNT', name, min, max)
def zincrby(self, name, value, amount=1):
"Increment the score of ``value`` in sorted set ``name`` by ``amount``"
return self.execute_command('ZINCRBY', name, amount, value)
def zrange(self, name, start, end, desc=False, withscores=False):
"""
Return a range of values from sorted set ``name`` between
``start`` and ``end`` sorted in ascending order.
``start`` and ``end`` can be negative, indicating the end of the range.
``desc`` a boolean indicating whether to sort the results descendingly
``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
"""
if desc:
return self.zrevrange(name, start, end, withscores,
score_cast_func)
pieces = ['ZRANGE', name, start, end]
if withscores:
pieces.append('withscores')
options = {
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options)
def zrangebyscore(self, name, min, max, start=None, num=None,
withscores=False):
"""
Return a range of values from the sorted set ``name`` with scores
between ``min`` and ``max``.
If ``start`` and ``num`` are specified, then return a slice
of the range.
``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs
`score_cast_func`` a callable used to cast the score return value
"""
if (start is not None and num is None) or \
(num is not None and start is None):
raise LedisError("``start`` and ``num`` must both be specified")
pieces = ['ZRANGEBYSCORE', name, min, max]
if start is not None and num is not None:
pieces.extend(['LIMIT', start, num])
if withscores:
pieces.append('withscores')
options = {
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options)
def zrank(self, name, value):
"""
Returns a 0-based value indicating the rank of ``value`` in sorted set
``name``
"""
return self.execute_command('ZRANK', name, value)
def zrem(self, name, *values):
"Remove member ``values`` from sorted set ``name``"
return self.execute_command('ZREM', name, *values)
def zremrangebyrank(self, name, min, max):
"""
Remove all elements in the sorted set ``name`` with ranks between
``min`` and ``max``. Values are 0-based, ordered from smallest score
to largest. Values can be negative indicating the highest scores.
Returns the number of elements removed
"""
return self.execute_command('ZREMRANGEBYRANK', name, min, max)
def zremrangebyscore(self, name, min, max):
"""
Remove all elements in the sorted set ``name`` with scores
between ``min`` and ``max``. Returns the number of elements removed.
"""
return self.execute_command('ZREMRANGEBYSCORE', name, min, max)
def zrevrange(self, name, start, num, withscores=False):
"""
Return a range of values from sorted set ``name`` between
``start`` and ``num`` sorted in descending order.
``start`` and ``num`` can be negative, indicating the end of the range.
``withscores`` indicates to return the scores along with the values
The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
"""
pieces = ['ZREVRANGE', name, start, num]
if withscores:
pieces.append('withscores')
options = {
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options)
def zrevrangebyscore(self, name, min, max, start=None, num=None,
withscores=False):
"""
Return a range of values from the sorted set ``name`` with scores
between ``min`` and ``max`` in descending order.
If ``start`` and ``num`` are specified, then return a slice
of the range.
``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
"""
if (start is not None and num is None) or \
(num is not None and start is None):
raise LedisError("``start`` and ``num`` must both be specified")
pieces = ['ZREVRANGEBYSCORE', name, min, max]
if start is not None and num is not None:
pieces.extend(['LIMIT', start, num])
if withscores:
pieces.append('withscores')
options = {
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options)
def zrevrank(self, name, value):
"""
Returns a 0-based value indicating the descending rank of
``value`` in sorted set ``name``
"""
return self.execute_command('ZREVRANK', name, value)
def zscore(self, name, value):
"Return the score of element ``value`` in sorted set ``name``"
return self.execute_command('ZSCORE', name, value)
# SPECIAL COMMANDS SUPPORTED BY LEDISDB
def zclear(self, name):
"Delete key of ``name`` from sorted set"
return self.execute_command('ZCLEAR', name)
def zmclear(self, *names):
"Delete multiple keys of ``names`` from sorted set"
return self.execute_command('ZMCLEAR', *names)
def zexpire(self, name, time):
"Set timeout on key ``name`` with ``time``"
if isinstance(time, datetime.timedelta):
time = time.seconds + time.days * 24 * 3600
return self.execute_command('ZEXPIRE', name, time)
def zexpireat(self, name, when):
"""
Set an expire flag on key name for time seconds. time can be represented by
an integer or a Python timedelta object.
"""
if isinstance(when, datetime.datetime):
when = int(mod_time.mktime(when.timetuple()))
return self.execute_command('ZEXPIREAT', name, when)
def zttl(self, name):
"Returns the number of seconds until the key name will expire"
return self.execute_command('ZTTL', name)
def zpersist(self, name):
"Removes an expiration on name"
return self.execute_command('ZPERSIST', name)
#### HASH COMMANDS ####
def hdel(self, name, *keys):
"Delete ``keys`` from hash ``name``"
return self.execute_command('HDEL', name, *keys)
def hexists(self, name, key):
"Returns a boolean indicating if ``key`` exists within hash ``name``"
return self.execute_command('HEXISTS', name, key)
def hget(self, name, key):
"Return the value of ``key`` within the hash ``name``"
return self.execute_command('HGET', name, key)
def hgetall(self, name):
"Return a Python dict of the hash's name/value pairs"
return self.execute_command('HGETALL', name)
def hincrby(self, name, key, amount=1):
"Increment the value of ``key`` in hash ``name`` by ``amount``"
return self.execute_command('HINCRBY', name, key, amount)
def hkeys(self, name):
"Return the list of keys within hash ``name``"
return self.execute_command('HKEYS', name)
def hlen(self, name):
"Return the number of elements in hash ``name``"
return self.execute_command('HLEN', name)
def hmget(self, name, keys, *args):
"Returns a list of values ordered identically to ``keys``"
args = list_or_args(keys, args)
return self.execute_command('HMGET', name, *args)
def hmset(self, name, mapping):
"""
Sets each key in the ``mapping`` dict to its corresponding value
in the hash ``name``
"""
if not mapping:
raise DataError("'hmset' with 'mapping' of length 0")
items = []
for pair in iteritems(mapping):
items.extend(pair)
return self.execute_command('HMSET', name, *items)
def hset(self, name, key, value):
"""
Set ``key`` to ``value`` within hash ``name``
Returns 1 if HSET created a new field, otherwise 0
"""
return self.execute_command('HSET', name, key, value)
def hvals(self, name):
"Return the list of values within hash ``name``"
return self.execute_command('HVALS', name)
# SPECIAL COMMANDS SUPPORTED BY LEDISDB
def hclear(self, name):
"Delete key ``name`` from hash"
return self.execute_command('HCLEAR', name)
def hmclear(self, *names):
"Delete multiple keys ``names`` from hash"
return self.execute_command('HMCLEAR', *names)
def hexpire(self, name, time):
"""
Set an expire flag on key name for time milliseconds.
time can be represented by an integer or a Python timedelta object.
"""
if isinstance(time, datetime.timedelta):
time = time.seconds + time.days * 24 * 3600
return self.execute_command('HEXPIRE', name, time)
def hexpireat(self, name, when):
"""
Set an expire flag on key name. when can be represented as an integer representing
unix time in milliseconds (unix time * 1000) or a Python datetime object.
"""
if isinstance(when, datetime.datetime):
when = int(mod_time.mktime(when.timetuple()))
return self.execute_command('HEXPIREAT', name, when)
def httl(self, name):
"Returns the number of seconds until the key name will expire"
return self.execute_command('HTTL', name)
def hpersist(self, name):
"Removes an expiration on name"
return self.execute_command('HPERSIST', name)

View File

@ -0,0 +1,606 @@
from itertools import chain
import os
import socket
import sys
from ledis._compat import (b, xrange, imap, byte_to_chr, unicode, bytes, long,
BytesIO, nativestr, basestring, iteritems,
LifoQueue, Empty, Full, urlparse, parse_qs)
from ledis.exceptions import (
LedisError,
ConnectionError,
BusyLoadingError,
ResponseError,
InvalidResponse,
ExecAbortError,
)
SYM_STAR = b('*')
SYM_DOLLAR = b('$')
SYM_CRLF = b('\r\n')
SYM_LF = b('\n')
class PythonParser(object):
"Plain Python parsing class"
MAX_READ_LENGTH = 1000000
encoding = None
EXCEPTION_CLASSES = {
'ERR': ResponseError,
'EXECABORT': ExecAbortError,
'LOADING': BusyLoadingError,
}
def __init__(self):
self._fp = None
def __del__(self):
try:
self.on_disconnect()
except Exception:
pass
def on_connect(self, connection):
"Called when the socket connects"
self._fp = connection._sock.makefile('rb')
if connection.decode_responses:
self.encoding = connection.encoding
def on_disconnect(self):
"Called when the socket disconnects"
if self._fp is not None:
self._fp.close()
self._fp = None
def read(self, length=None):
"""
Read a line from the socket if no length is specified,
otherwise read ``length`` bytes. Always strip away the newlines.
"""
try:
if length is not None:
bytes_left = length + 2 # read the line ending
if length > self.MAX_READ_LENGTH:
# apparently reading more than 1MB or so from a windows
# socket can cause MemoryErrors. See:
# https://github.com/andymccurdy/redis-py/issues/205
# read smaller chunks at a time to work around this
try:
buf = BytesIO()
while bytes_left > 0:
read_len = min(bytes_left, self.MAX_READ_LENGTH)
buf.write(self._fp.read(read_len))
bytes_left -= read_len
buf.seek(0)
return buf.read(length)
finally:
buf.close()
return self._fp.read(bytes_left)[:-2]
# no length, read a full line
return self._fp.readline()[:-2]
except (socket.error, socket.timeout):
e = sys.exc_info()[1]
raise ConnectionError("Error while reading from socket: %s" %
(e.args,))
def parse_error(self, response):
"Parse an error response"
error_code = response.split(' ')[0]
if error_code in self.EXCEPTION_CLASSES:
response = response[len(error_code) + 1:]
return self.EXCEPTION_CLASSES[error_code](response)
return ResponseError(response)
def read_response(self):
response = self.read()
if not response:
raise ConnectionError("Socket closed on remote end")
byte, response = byte_to_chr(response[0]), response[1:]
if byte not in ('-', '+', ':', '$', '*'):
raise InvalidResponse("Protocol Error")
# server returned an error
if byte == '-':
response = nativestr(response)
error = self.parse_error(response)
# if the error is a ConnectionError, raise immediately so the user
# is notified
if isinstance(error, ConnectionError):
raise error
# otherwise, we're dealing with a ResponseError that might belong
# inside a pipeline response. the connection's read_response()
# and/or the pipeline's execute() will raise this error if
# necessary, so just return the exception instance here.
return error
# single value
elif byte == '+':
pass
# int value
elif byte == ':':
response = long(response)
# bulk response
elif byte == '$':
length = int(response)
if length == -1:
return None
response = self.read(length)
# multi-bulk response
elif byte == '*':
length = int(response)
if length == -1:
return None
response = [self.read_response() for i in xrange(length)]
if isinstance(response, bytes) and self.encoding:
response = response.decode(self.encoding)
return response
DefaultParser = PythonParser
class Connection(object):
"Manages TCP communication to and from a Ledis server"
def __init__(self, host='localhost', port=6380, db=0, password=None,
socket_timeout=None, encoding='utf-8',
encoding_errors='strict', decode_responses=False,
parser_class=DefaultParser):
self.pid = os.getpid()
self.host = host
self.port = port
self.db = db
self.password = password
self.socket_timeout = socket_timeout
self.encoding = encoding
self.encoding_errors = encoding_errors
self.decode_responses = decode_responses
self._sock = None
self._parser = parser_class()
def __del__(self):
try:
self.disconnect()
except Exception:
pass
def connect(self):
"Connects to the Ledis server if not already connected"
if self._sock:
return
try:
sock = self._connect()
except socket.error:
e = sys.exc_info()[1]
raise ConnectionError(self._error_message(e))
self._sock = sock
self.on_connect()
def _connect(self):
"Create a TCP socket connection"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.socket_timeout)
sock.connect((self.host, self.port))
return sock
def _error_message(self, exception):
# args for socket.error can either be (errno, "message")
# or just "message"
if len(exception.args) == 1:
return "Error connecting to %s:%s. %s." % \
(self.host, self.port, exception.args[0])
else:
return "Error %s connecting %s:%s. %s." % \
(exception.args[0], self.host, self.port, exception.args[1])
def on_connect(self):
"Initialize the connection, authenticate and select a database"
self._parser.on_connect(self)
# if a password is specified, authenticate
if self.password:
self.send_command('AUTH', self.password)
if nativestr(self.read_response()) != 'OK':
raise AuthenticationError('Invalid Password')
# if a database is specified, switch to it
if self.db:
self.send_command('SELECT', self.db)
if nativestr(self.read_response()) != 'OK':
raise ConnectionError('Invalid Database')
def disconnect(self):
"Disconnects from the Ledis server"
self._parser.on_disconnect()
if self._sock is None:
return
try:
self._sock.close()
except socket.error:
pass
self._sock = None
def send_packed_command(self, command):
"Send an already packed command to the Ledis server"
if not self._sock:
self.connect()
try:
self._sock.sendall(command)
except socket.error:
e = sys.exc_info()[1]
self.disconnect()
if len(e.args) == 1:
_errno, errmsg = 'UNKNOWN', e.args[0]
else:
_errno, errmsg = e.args
raise ConnectionError("Error %s while writing to socket. %s." %
(_errno, errmsg))
except Exception:
self.disconnect()
raise
def send_command(self, *args):
"Pack and send a command to the Ledis server"
self.send_packed_command(self.pack_command(*args))
def read_response(self):
"Read the response from a previously sent command"
try:
response = self._parser.read_response()
except Exception:
self.disconnect()
raise
if isinstance(response, ResponseError):
raise response
return response
def encode(self, value):
"Return a bytestring representation of the value"
if isinstance(value, bytes):
return value
if isinstance(value, float):
value = repr(value)
if not isinstance(value, basestring):
value = str(value)
if isinstance(value, unicode):
value = value.encode(self.encoding, self.encoding_errors)
return value
def pack_command(self, *args):
"Pack a series of arguments into a value Ledis command"
output = SYM_STAR + b(str(len(args))) + SYM_CRLF
for enc_value in imap(self.encode, args):
output += SYM_DOLLAR
output += b(str(len(enc_value)))
output += SYM_CRLF
output += enc_value
output += SYM_CRLF
return output
class UnixDomainSocketConnection(Connection):
def __init__(self, path='', db=0, password=None,
socket_timeout=None, encoding='utf-8',
encoding_errors='strict', decode_responses=False,
parser_class=DefaultParser):
self.pid = os.getpid()
self.path = path
self.db = db
self.password = password
self.socket_timeout = socket_timeout
self.encoding = encoding
self.encoding_errors = encoding_errors
self.decode_responses = decode_responses
self._sock = None
self._parser = parser_class()
def _connect(self):
"Create a Unix domain socket connection"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.socket_timeout)
sock.connect(self.path)
return sock
def _error_message(self, exception):
# args for socket.error can either be (errno, "message")
# or just "message"
if len(exception.args) == 1:
return "Error connecting to unix socket: %s. %s." % \
(self.path, exception.args[0])
else:
return "Error %s connecting to unix socket: %s. %s." % \
(exception.args[0], self.path, exception.args[1])
# TODO: add ability to block waiting on a connection to be released
class ConnectionPool(object):
"Generic connection pool"
@classmethod
def from_url(cls, url, db=None, **kwargs):
"""
Return a connection pool configured from the given URL.
For example::
redis://[:password]@localhost:6379/0
rediss://[:password]@localhost:6379/0
unix://[:password]@/path/to/socket.sock?db=0
Three URL schemes are supported:
redis:// creates a normal TCP socket connection
rediss:// creates a SSL wrapped TCP socket connection
unix:// creates a Unix Domain Socket connection
There are several ways to specify a database number. The parse function
will return the first specified option:
1. A ``db`` querystring option, e.g. redis://localhost?db=0
2. If using the redis:// scheme, the path argument of the url, e.g.
redis://localhost/0
3. The ``db`` argument to this function.
If none of these options are specified, db=0 is used.
Any additional querystring arguments and keyword arguments will be
passed along to the ConnectionPool class's initializer. In the case
of conflicting arguments, querystring arguments always win.
"""
url_string = url
url = urlparse(url)
qs = ''
# in python2.6, custom URL schemes don't recognize querystring values
# they're left as part of the url.path.
if '?' in url.path and not url.query:
# chop the querystring including the ? off the end of the url
# and reparse it.
qs = url.path.split('?', 1)[1]
url = urlparse(url_string[:-(len(qs) + 1)])
else:
qs = url.query
url_options = {}
for name, value in iteritems(parse_qs(qs)):
if value and len(value) > 0:
url_options[name] = value[0]
# We only support redis:// and unix:// schemes.
if url.scheme == 'unix':
url_options.update({
'password': url.password,
'path': url.path,
'connection_class': UnixDomainSocketConnection,
})
else:
url_options.update({
'host': url.hostname,
'port': int(url.port or 6380),
'password': url.password,
})
# If there's a path argument, use it as the db argument if a
# querystring value wasn't specified
if 'db' not in url_options and url.path:
try:
url_options['db'] = int(url.path.replace('/', ''))
except (AttributeError, ValueError):
pass
if url.scheme == 'lediss':
url_options['connection_class'] = SSLConnection
# last shot at the db value
url_options['db'] = int(url_options.get('db', db or 0))
# update the arguments from the URL values
kwargs.update(url_options)
return cls(**kwargs)
def __init__(self, connection_class=Connection, max_connections=None,
**connection_kwargs):
self.pid = os.getpid()
self.connection_class = connection_class
self.connection_kwargs = connection_kwargs
self.max_connections = max_connections or 2 ** 31
self._created_connections = 0
self._available_connections = []
self._in_use_connections = set()
def _checkpid(self):
if self.pid != os.getpid():
self.disconnect()
self.__init__(self.connection_class, self.max_connections,
**self.connection_kwargs)
def get_connection(self, command_name, *keys, **options):
"Get a connection from the pool"
self._checkpid()
try:
connection = self._available_connections.pop()
except IndexError:
connection = self.make_connection()
self._in_use_connections.add(connection)
return connection
def make_connection(self):
"Create a new connection"
if self._created_connections >= self.max_connections:
raise ConnectionError("Too many connections")
self._created_connections += 1
return self.connection_class(**self.connection_kwargs)
def release(self, connection):
"Releases the connection back to the pool"
self._checkpid()
if connection.pid == self.pid:
self._in_use_connections.remove(connection)
self._available_connections.append(connection)
def disconnect(self):
"Disconnects all connections in the pool"
all_conns = chain(self._available_connections,
self._in_use_connections)
for connection in all_conns:
connection.disconnect()
class BlockingConnectionPool(object):
"""
Thread-safe blocking connection pool::
>>> from redis.client import Redis
>>> client = Redis(connection_pool=BlockingConnectionPool())
It performs the same function as the default
``:py:class: ~redis.connection.ConnectionPool`` implementation, in that,
it maintains a pool of reusable connections that can be shared by
multiple redis clients (safely across threads if required).
The difference is that, in the event that a client tries to get a
connection from the pool when all of connections are in use, rather than
raising a ``:py:class: ~redis.exceptions.ConnectionError`` (as the default
``:py:class: ~redis.connection.ConnectionPool`` implementation does), it
makes the client wait ("blocks") for a specified number of seconds until
a connection becomes available.
Use ``max_connections`` to increase / decrease the pool size::
>>> pool = BlockingConnectionPool(max_connections=10)
Use ``timeout`` to tell it either how many seconds to wait for a connection
to become available, or to block forever:
# Block forever.
>>> pool = BlockingConnectionPool(timeout=None)
# Raise a ``ConnectionError`` after five seconds if a connection is
# not available.
>>> pool = BlockingConnectionPool(timeout=5)
"""
def __init__(self, max_connections=50, timeout=20, connection_class=None,
queue_class=None, **connection_kwargs):
"Compose and assign values."
# Compose.
if connection_class is None:
connection_class = Connection
if queue_class is None:
queue_class = LifoQueue
# Assign.
self.connection_class = connection_class
self.connection_kwargs = connection_kwargs
self.queue_class = queue_class
self.max_connections = max_connections
self.timeout = timeout
# Validate the ``max_connections``. With the "fill up the queue"
# algorithm we use, it must be a positive integer.
is_valid = isinstance(max_connections, int) and max_connections > 0
if not is_valid:
raise ValueError('``max_connections`` must be a positive integer')
# Get the current process id, so we can disconnect and reinstantiate if
# it changes.
self.pid = os.getpid()
# Create and fill up a thread safe queue with ``None`` values.
self.pool = self.queue_class(max_connections)
while True:
try:
self.pool.put_nowait(None)
except Full:
break
# Keep a list of actual connection instances so that we can
# disconnect them later.
self._connections = []
def _checkpid(self):
"""
Check the current process id. If it has changed, disconnect and
re-instantiate this connection pool instance.
"""
# Get the current process id.
pid = os.getpid()
# If it hasn't changed since we were instantiated, then we're fine, so
# just exit, remaining connected.
if self.pid == pid:
return
# If it has changed, then disconnect and re-instantiate.
self.disconnect()
self.reinstantiate()
def make_connection(self):
"Make a fresh connection."
connection = self.connection_class(**self.connection_kwargs)
self._connections.append(connection)
return connection
def get_connection(self, command_name, *keys, **options):
"""
Get a connection, blocking for ``self.timeout`` until a connection
is available from the pool.
If the connection returned is ``None`` then creates a new connection.
Because we use a last-in first-out queue, the existing connections
(having been returned to the pool after the initial ``None`` values
were added) will be returned before ``None`` values. This means we only
create new connections when we need to, i.e.: the actual number of
connections will only increase in response to demand.
"""
# Make sure we haven't changed process.
self._checkpid()
# Try and get a connection from the pool. If one isn't available within
# self.timeout then raise a ``ConnectionError``.
connection = None
try:
connection = self.pool.get(block=True, timeout=self.timeout)
except Empty:
# Note that this is not caught by the redis client and will be
# raised unless handled by application code. If you want never to
raise ConnectionError("No connection available.")
# If the ``connection`` is actually ``None`` then that's a cue to make
# a new connection to add to the pool.
if connection is None:
connection = self.make_connection()
return connection
def release(self, connection):
"Releases the connection back to the pool."
# Make sure we haven't changed process.
self._checkpid()
# Put the connection back into the pool.
try:
self.pool.put_nowait(connection)
except Full:
# This shouldn't normally happen but might perhaps happen after a
# reinstantiation. So, we can handle the exception by not putting
# the connection back on the pool, because we definitely do not
# want to reuse it.
pass
def disconnect(self):
"Disconnects all connections in the pool."
for connection in self._connections:
connection.disconnect()
def reinstantiate(self):
"""
Reinstatiate this instance within a new process with a new connection
pool set.
"""
self.__init__(max_connections=self.max_connections,
timeout=self.timeout,
connection_class=self.connection_class,
queue_class=self.queue_class, **self.connection_kwargs)

View File

@ -0,0 +1,32 @@
"Core exceptions raised by the LedisDB client"
class LedisError(Exception):
pass
class ServerError(LedisError):
pass
class ConnectionError(ServerError):
pass
class BusyLoadingError(ConnectionError):
pass
class InvalidResponse(ServerError):
pass
class ResponseError(LedisError):
pass
class DataError(LedisError):
pass
class ExecAbortError(ResponseError):
pass

View File

@ -0,0 +1,10 @@
def from_url(url, db=None, **kwargs):
"""
Returns an active Ledis client generated from the given database URL.
Will attempt to extract the database id from the path url fragment, if
none is provided.
"""
from ledis.client import Ledis
return Ledis.from_url(url, db, **kwargs)

View File

@ -0,0 +1,22 @@
Copyright (c) 2012 Andy McCurdy
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.

50
client/ledis-py/setup.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
import os
import sys
from ledis import __version__
try:
from setuptools import setup
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
# import here, because outside the eggs aren't loaded
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
except ImportError:
from distutils.core import setup
PyTest = lambda x: x
setup(
name='ledis',
version=__version__,
description='Python client for ledisdb key-value database',
long_description='Python client for ledisdb key-value database',
url='https://github.com/siddontang/ledisdb',
keywords=['ledis', 'key-value store'],
license='MIT',
packages=['ledis'],
tests_require=['pytest>=2.5.0'],
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
]
)

View File

View File

@ -0,0 +1,145 @@
# coding: utf-8
# Test Cases for hash commands
import unittest
import sys
import datetime, time
sys.path.append('..')
import ledis
from ledis._compat import b, iteritems, itervalues
from ledis import ResponseError
l = ledis.Ledis(port=6380)
def current_time():
return datetime.datetime.now()
class TestCmdHash(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
l.hmclear('myhash', 'a')
def test_hdel(self):
l.hset('myhash', 'field1', 'foo')
assert l.hdel('myhash', 'field1') == 1
assert l.hdel('myhash', 'field1') == 0
assert l.hdel('myhash', 'field1', 'field2') == 0
def test_hexists(self):
l.hset('myhash', 'field1', 'foo')
l.hdel('myhash', 'field2')
assert l.hexists('myhash', 'field1') == 1
assert l.hexists('myhash', 'field2') == 0
def test_hget(self):
l.hset('myhash', 'field1', 'foo')
assert l.hget('myhash', 'field1') == 'foo'
assert (l.hget('myhash', 'field2')) is None
def test_hgetall(self):
h = {'field1': 'foo', 'field2': 'bar'}
l.hmset('myhash', h)
assert l.hgetall('myhash') == h
def test_hincrby(self):
assert l.hincrby('myhash', 'field1') == 1
l.hclear('myhash')
assert l.hincrby('myhash', 'field1', 1) == 1
assert l.hincrby('myhash', 'field1', 5) == 6
assert l.hincrby('myhash', 'field1', -10) == -4
def test_hkeys(self):
h = {'field1': 'foo', 'field2': 'bar'}
l.hmset('myhash', h)
assert l.hkeys('myhash') == ['field1', 'field2']
def test_hlen(self):
l.hset('myhash', 'field1', 'foo')
assert l.hlen('myhash') == 1
l.hset('myhash', 'field2', 'bar')
assert l.hlen('myhash') == 2
def test_hmget(self):
assert l.hmset('myhash', {'a': '1', 'b': '2', 'c': '3'})
assert l.hmget('myhash', 'a', 'b', 'c') == ['1', '2', '3']
def test_hmset(self):
h = {'a': '1', 'b': '2', 'c': '3'}
assert l.hmset('myhash', h)
assert l.hgetall('myhash') == h
def test_hset(self):
l.hclear('myhash')
assert int(l.hset('myhash', 'field1', 'foo')) == 1
assert l.hset('myhash', 'field1', 'foo') == 0
def test_hvals(self):
h = {'a': '1', 'b': '2', 'c': '3'}
l.hmset('myhash', h)
local_vals = list(itervalues(h))
remote_vals = l.hvals('myhash')
assert sorted(local_vals) == sorted(remote_vals)
def test_hclear(self):
h = {'a': '1', 'b': '2', 'c': '3'}
l.hmset('myhash', h)
assert l.hclear('myhash') == 3
assert l.hclear('myhash') == 0
def test_hmclear(self):
h = {'a': '1', 'b': '2', 'c': '3'}
l.hmset('myhash1', h)
l.hmset('myhash2', h)
assert l.hmclear('myhash1', 'myhash2') == 2
def test_hexpire(self):
assert l.hexpire('myhash', 100) == 0
l.hset('myhash', 'field1', 'foo')
assert l.hexpire('myhash', 100) == 1
assert l.httl('myhash') <= 100
def test_hexpireat_datetime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.hset('a', 'f', 'foo')
assert l.hexpireat('a', expire_at)
assert 0 < l.httl('a') <= 61
def test_hexpireat_unixtime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.hset('a', 'f', 'foo')
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
assert l.hexpireat('a', expire_at_seconds)
assert 0 < l.httl('a') <= 61
def test_zexpireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.hexpireat('a', expire_at)
def test_hexpireat(self):
assert l.hexpireat('myhash', 1577808000) == 0
l.hset('myhash', 'field1', 'foo')
assert l.hexpireat('myhash', 1577808000) == 1
def test_httl(self):
l.hset('myhash', 'field1', 'foo')
assert l.hexpire('myhash', 100)
assert l.httl('myhash') <= 100
def test_hpersist(self):
l.hset('myhash', 'field1', 'foo')
l.hexpire('myhash', 100)
assert l.httl('myhash') <= 100
assert l.hpersist('myhash')
assert l.httl('myhash') == -1

View File

@ -0,0 +1,158 @@
# coding: utf-8
# Test Cases for k/v commands
import unittest
import sys
import datetime, time
sys.path.append('..')
import ledis
from ledis._compat import b, iteritems
l = ledis.Ledis(port=6380)
def current_time():
return datetime.datetime.now()
class TestCmdKv(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
l.delete('a', 'b', 'c', 'non_exist_key')
def test_decr(self):
assert l.delete('a') == 1
assert l.decr('a') == -1
assert l['a'] == b('-1')
assert l.decr('a') == -2
assert l['a'] == b('-2')
assert l.decr('a', amount=5) == -7
assert l['a'] == b('-7')
#FIXME: how to test exception?
# l.set('b', '234293482390480948029348230948')
# self.assertRaises(ResponseError, l.delete('b'))
def test_decrby(self):
assert l.delete('a') == 1
assert l.decrby('a') == -1
assert l['a'] == b('-1')
assert l.decrby('a') == -2
assert l['a'] == b('-2')
assert l.decrby('a', amount=5) == -7
assert l['a'] == b('-7')
def test_del(self):
assert l.delete('a') == 1
assert l.delete('a', 'b', 'c') == 3
def test_exists(self):
l.delete('a', 'non_exist_key')
l.set('a', 'hello')
assert (l.exists('a'))
assert not (l.exists('non_exist_key'))
def test_get(self):
l.set('a', 'hello')
assert l.get('a') == 'hello'
l.set('b', '中文')
assert l.get('b') == '中文'
l.delete('non_exist_key')
assert (l.get('non_exist_key')) is None
def test_getset(self):
l.set('a', 'hello')
assert l.getset('a', 'world') == 'hello'
assert l.get('a') == 'world'
l.delete('non_exist_key')
assert (l.getset('non_exist_key', 'non')) is None
def test_incr(self):
l.delete('non_exist_key')
assert l.incr('non_exist_key') == 1
l.set('a', 100)
assert l.incr('a') == 101
def test_incrby(self):
l.delete('a')
assert l.incrby('a', 100) == 100
l.set('a', 100)
assert l.incrby('a', 100) == 200
assert l.incrby('a', amount=100) == 300
def test_mget(self):
l.set('a', 'hello')
l.set('b', 'world')
l.delete('non_exist_key')
assert l.mget('a', 'b', 'non_exist_key') == ['hello', 'world', None]
l.delete('a', 'b')
assert l.mget(['a', 'b']) == [None, None]
def test_mset(self):
d = {'a': b('1'), 'b': b('2'), 'c': b('3')}
assert l.mset(**d)
for k, v in iteritems(d):
assert l[k] == v
def test_set(self):
assert (l.set('a', 100))
def test_setnx(self):
l.delete('a')
assert l.setnx('a', '1')
assert l['a'] == b('1')
assert not l.setnx('a', '2')
assert l['a'] == b('1')
def test_ttl(self):
assert l.set('a', 'hello')
assert l.expire('a', 100)
assert l.ttl('a') <= 100
l.delete('a')
assert l.ttl('a') == -1
l.set('a', 'hello')
assert l.ttl('a') == -1
def test_persist(self):
assert l.set('a', 'hello')
assert l.expire('a', 100)
assert l.ttl('a') <= 100
assert l.persist('a')
l.delete('non_exist_key')
assert not l.persist('non_exist_key')
def test_expire(self):
assert not l.expire('a', 100)
l.set('a', 'hello')
assert (l.expire('a', 100))
l.delete('a')
assert not (l.expire('a', 100))
def test_expireat_datetime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.set('a', '1')
assert l.expireat('a', expire_at)
assert 0 < l.ttl('a') <= 61
def test_expireat_unixtime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.set('a', '1')
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
assert l.expireat('a', expire_at_seconds)
assert 0 < l.ttl('a') <= 61
def test_expireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.expireat('a', expire_at)
def test_expireat(self):
l.set('a', 'hello')
assert (l.expireat('a', 1577808000)) # time is 2020.1.1
l.delete('a')
assert not(l.expireat('a', 1577808000))

View File

@ -0,0 +1,109 @@
# coding: utf-8
# Test Cases for list commands
import unittest
import datetime, time
import sys
sys.path.append('..')
import ledis
from ledis._compat import b
l = ledis.Ledis(port=6380)
def current_time():
return datetime.datetime.now()
class TestCmdList(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
l.lmclear('mylist', 'mylist1', 'mylist2')
def test_lindex(self):
l.rpush('mylist', '1', '2', '3')
assert l.lindex('mylist', 0) == b('1')
assert l.lindex('mylist', 1) == b('2')
assert l.lindex('mylist', 2) == b('3')
def test_llen(self):
l.rpush('mylist', '1', '2', '3')
assert l.llen('mylist') == 3
def test_lpop(self):
l.rpush('mylist', '1', '2', '3')
assert l.lpop('mylist') == b('1')
assert l.lpop('mylist') == b('2')
assert l.lpop('mylist') == b('3')
assert l.lpop('mylist') is None
def test_lpush(self):
assert l.lpush('mylist', '1') == 1
assert l.lpush('mylist', '2') == 2
assert l.lpush('mylist', '3', '4', '5') == 5
assert l.lrange('mylist', 0, -1) == ['5', '4', '3', '2', '1']
def test_lrange(self):
l.rpush('mylist', '1', '2', '3', '4', '5')
assert l.lrange('mylist', 0, 2) == ['1', '2', '3']
assert l.lrange('mylist', 2, 10) == ['3', '4', '5']
assert l.lrange('mylist', 0, -1) == ['1', '2', '3', '4', '5']
def test_rpush(self):
assert l.rpush('mylist', '1') == 1
assert l.rpush('mylist', '2') == 2
assert l.rpush('mylist', '3', '4') == 4
assert l.lrange('mylist', 0, -1) == ['1', '2', '3', '4']
def test_rpop(self):
l.rpush('mylist', '1', '2', '3')
assert l.rpop('mylist') == b('3')
assert l.rpop('mylist') == b('2')
assert l.rpop('mylist') == b('1')
assert l.rpop('mylist') is None
def test_lclear(self):
l.rpush('mylist', '1', '2', '3')
assert l.lclear('mylist') == 3
assert l.lclear('mylist') == 0
def test_lmclear(self):
l.rpush('mylist1', '1', '2', '3')
l.rpush('mylist2', '1', '2', '3')
assert l.lmclear('mylist1', 'mylist2') == 2
def test_lexpire(self):
assert not l.lexpire('mylist', 100)
l.rpush('mylist', '1')
assert l.lexpire('mylist', 100)
assert 0 < l.lttl('mylist') <= 100
assert l.lpersist('mylist')
assert l.lttl('mylist') == -1
def test_lexpireat_datetime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.rpush('mylist', '1')
assert l.lexpireat('mylist', expire_at)
assert 0 < l.lttl('mylist') <= 61
def test_lexpireat_unixtime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.rpush('mylist', '1')
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
assert l.lexpireat('mylist', expire_at_seconds)
assert l.lttl('mylist') <= 61
def test_lexpireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.lexpireat('mylist', expire_at)
def test_lttl_and_lpersist(self):
l.rpush('mylist', '1')
l.lexpire('mylist', 100)
assert 0 < l.lttl('mylist') <= 100
assert l.lpersist('mylist')
assert l.lttl('mylist') == -1

View File

@ -0,0 +1,169 @@
# coding: utf-8
# Test Cases for zset commands
import unittest
import sys
import datetime, time
sys.path.append('..')
import ledis
from ledis._compat import b, iteritems
from ledis import ResponseError
l = ledis.Ledis(port=6380)
def current_time():
return datetime.datetime.now()
class TestCmdZset(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
l.zclear('a')
def test_zadd(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zrange('a', 0, -1) == [b('a1'), b('a2'), b('a3')]
def test_zcard(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zcard('a') == 3
def test_zcount(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zcount('a', '-inf', '+inf') == 3
assert l.zcount('a', 1, 2) == 2
assert l.zcount('a', 10, 20) == 0
def test_zincrby(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zincrby('a', 'a2') == 3
assert l.zincrby('a', 'a3', amount=5) == 8
assert l.zscore('a', 'a2') == 3
assert l.zscore('a', 'a3') == 8
def test_zrange(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zrange('a', 0, 1) == [b('a1'), b('a2')]
assert l.zrange('a', 2, 3) == [b('a3')]
#withscores
assert l.zrange('a', 0, 1, withscores=True) == \
[(b('a1'), 1), (b('a2'), 2)]
assert l.zrange('a', 2, 3, withscores=True) == \
[(b('a3'), 3)]
def test_zrangebyscore(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zrangebyscore('a', 2, 4) == [b('a2'), b('a3'), b('a4')]
# slicing with start/num
assert l.zrangebyscore('a', 2, 4, start=1, num=2) == \
[b('a3'), b('a4')]
# withscores
assert l.zrangebyscore('a', 2, 4, withscores=True) == \
[('a2', 2), ('a3', 3), ('a4', 4)]
def test_zrank(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zrank('a', 'a1') == 0
assert l.zrank('a', 'a3') == 2
assert l.zrank('a', 'a6') is None
def test_zrem(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zrem('a', 'a2') == 1
assert l.zrange('a', 0, -1) == [b('a1'), b('a3')]
assert l.zrem('a', 'b') == 0
assert l.zrange('a', 0, -1) == [b('a1'), b('a3')]
# multiple keys
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zrem('a', 'a1', 'a2') == 2
assert l.zrange('a', 0, -1) == [b('a3')]
def test_zremrangebyrank(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zremrangebyrank('a', 1, 3) == 3
assert l.zrange('a', 0, -1) == [b('a1'), b('a5')]
def test_zremrangebyscore(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zremrangebyscore('a', 2, 4) == 3
assert l.zrange('a', 0, -1) == [b('a1'), b('a5')]
assert l.zremrangebyscore('a', 2, 4) == 0
assert l.zrange('a', 0, -1) == [b('a1'), b('a5')]
def test_zrevrange(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zrevrange('a', 0, 1) == [b('a3'), b('a2')]
assert l.zrevrange('a', 1, 2) == [b('a2'), b('a1')]
def test_zrevrank(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zrevrank('a', 'a1') == 4
assert l.zrevrank('a', 'a2') == 3
assert l.zrevrank('a', 'a6') is None
def test_zrevrangebyscore(self):
l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5)
assert l.zrevrangebyscore('a', 4, 2) == [b('a4'), b('a3'), b('a2')]
# slicing with start/num
assert l.zrevrangebyscore('a', 4, 2, start=1, num=2) == \
[b('a3'), b('a2')]
# withscores
assert l.zrevrangebyscore('a', 4, 2, withscores=True) == \
[(b('a4'), 4), (b('a3'), 3), (b('a2'), 2)]
def test_zscore(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zscore('a', 'a1') == 1
assert l.zscore('a', 'a2') == 2
assert l.zscore('a', 'a4') is None
def test_zclear(self):
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zclear('a') == 3
assert l.zclear('a') == 0
def test_zmclear(self):
l.zadd('a', a1=1, a2=2, a3=3)
l.zadd('b', b1=1, b2=2, b3=3)
assert l.lmclear('a', 'b') == 2
assert l.lmclear('c', 'd') == 2
def test_zexpire(self):
assert not l.zexpire('a', 100)
l.zadd('a', a1=1, a2=2, a3=3)
assert l.zexpire('a', 100)
assert 0 < l.zttl('a') <= 100
assert l.zpersist('a')
assert l.zttl('a') == -1
def test_zexpireat_datetime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.zadd('a', a1=1)
assert l.zexpireat('a', expire_at)
assert 0 < l.zttl('a') <= 61
def test_zexpireat_unixtime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.zadd('a', a1=1)
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
assert l.zexpireat('a', expire_at_seconds)
assert 0 < l.zttl('a') <= 61
def test_zexpireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.zexpireat('a', expire_at)
def test_zttl_and_zpersist(self):
l.zadd('a', a1=1)
l.zexpire('a', 100)
assert 0 < l.zttl('a') <= 100
assert l.zpersist('a')
assert l.zttl('a') == -1

View File

@ -0,0 +1,31 @@
# coding: utf-8
# Test Cases for other commands
import unittest
import sys
sys.path.append('..')
import ledis
from ledis._compat import b
from ledis import ResponseError
l = ledis.Ledis(port=6380)
class TestOtherCommands(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
# server information
def test_echo(self):
assert l.echo('foo bar') == b('foo bar')
def test_ping(self):
assert l.ping()
def test_select(self):
assert l.select('1')
assert l.select('15')
self.assertRaises(ResponseError, lambda: l.select('16'))

383
client/openresty/ledis.lua Normal file
View File

@ -0,0 +1,383 @@
--- refer from openresty redis lib
local sub = string.sub
local byte = string.byte
local tcp = ngx.socket.tcp
local concat = table.concat
local null = ngx.null
local pairs = pairs
local unpack = unpack
local setmetatable = setmetatable
local tonumber = tonumber
local error = error
local ok, new_tab = pcall(require, "table.new")
if not ok then
new_tab = function (narr, nrec) return {} end
end
local _M = new_tab(0, 155)
_M._VERSION = '0.01'
local commands = {
--[[kv]]
"decr",
"decrby",
"del",
"exists",
"get",
"getset",
"incr",
"incrby",
"mget",
"mset",
"set",
"setnx",
"ttl",
"expire",
"expireat",
"persist",
--[[hash]]
"hdel",
"hexists",
"hget",
"hgetall",
"hincrby",
"hkeys",
"hlen",
"hmget",
--[["hmset",]]
"hset",
"hvals",
--[[ledisdb special commands]]
"hclear",
"hmclear",
"hexpire",
"hexpireat",
"httl",
"hpersist",
--[[list]]
"lindex",
"llen",
"lpop",
"lpush",
"lrange",
"rpop",
"rpush",
--[[ledisdb special commands]]
"lclear",
"lmclear",
"lexpire",
"lexpireat",
"lttl",
"lpersist",
--[[zset]]
"zadd",
"zcard",
"zcount",
"zincrby",
"zrange",
"zrangebyscore",
"zrank",
"zrem",
"zremrangebyrank",
"zremrangebyscore",
"zrevrange",
"zrevrank",
"zrevrangebyscore",
"zscore",
--[[ledisdb special commands]]
"zclear",
"zmclear",
"zexpire",
"zexpireat",
"zttl",
"zpersist",
--[[server]]
"ping",
"echo",
"select"
}
local mt = { __index = _M }
function _M.new(self)
local sock, err = tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock }, mt)
end
function _M.set_timeout(self, timeout)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeout(timeout)
end
function _M.connect(self, ...)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:connect(...)
end
function _M.set_keepalive(self, ...)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:setkeepalive(...)
end
function _M.get_reused_times(self)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:getreusedtimes()
end
local function close(self)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:close()
end
_M.close = close
local function _read_reply(self, sock)
local line, err = sock:receive()
if not line then
if err == "timeout" then
sock:close()
end
return nil, err
end
local prefix = byte(line)
if prefix == 36 then -- char '$'
-- print("bulk reply")
local size = tonumber(sub(line, 2))
if size < 0 then
return null
end
local data, err = sock:receive(size)
if not data then
if err == "timeout" then
sock:close()
end
return nil, err
end
local dummy, err = sock:receive(2) -- ignore CRLF
if not dummy then
return nil, err
end
return data
elseif prefix == 43 then -- char '+'
-- print("status reply")
return sub(line, 2)
elseif prefix == 42 then -- char '*'
local n = tonumber(sub(line, 2))
-- print("multi-bulk reply: ", n)
if n < 0 then
return null
end
local vals = new_tab(n, 0);
local nvals = 0
for i = 1, n do
local res, err = _read_reply(self, sock)
if res then
nvals = nvals + 1
vals[nvals] = res
elseif res == nil then
return nil, err
else
-- be a valid redis error value
nvals = nvals + 1
vals[nvals] = {false, err}
end
end
return vals
elseif prefix == 58 then -- char ':'
-- print("integer reply")
return tonumber(sub(line, 2))
elseif prefix == 45 then -- char '-'
-- print("error reply: ", n)
return false, sub(line, 2)
else
return nil, "unkown prefix: \"" .. prefix .. "\""
end
end
local function _gen_req(args)
local nargs = #args
local req = new_tab(nargs + 1, 0)
req[1] = "*" .. nargs .. "\r\n"
local nbits = 1
for i = 1, nargs do
local arg = args[i]
nbits = nbits + 1
if not arg then
req[nbits] = "$-1\r\n"
else
if type(arg) ~= "string" then
arg = tostring(arg)
end
req[nbits] = "$" .. #arg .. "\r\n" .. arg .. "\r\n"
end
end
-- it is faster to do string concatenation on the Lua land
return concat(req)
end
local function _do_cmd(self, ...)
local args = {...}
local sock = self.sock
if not sock then
return nil, "not initialized"
end
local req = _gen_req(args)
local reqs = self._reqs
if reqs then
reqs[#reqs + 1] = req
return
end
-- print("request: ", table.concat(req))
local bytes, err = sock:send(req)
if not bytes then
return nil, err
end
return _read_reply(self, sock)
end
function _M.read_reply(self)
local sock = self.sock
if not sock then
return nil, "not initialized"
end
local res, err = _read_reply(self, sock)
return res, err
end
for i = 1, #commands do
local cmd = commands[i]
_M[cmd] =
function (self, ...)
return _do_cmd(self, cmd, ...)
end
end
function _M.hmset(self, hashname, ...)
local args = {...}
if #args == 1 then
local t = args[1]
local n = 0
for k, v in pairs(t) do
n = n + 2
end
local array = new_tab(n, 0)
local i = 0
for k, v in pairs(t) do
array[i + 1] = k
array[i + 2] = v
i = i + 2
end
-- print("key", hashname)
return _do_cmd(self, "hmset", hashname, unpack(array))
end
-- backwards compatibility
return _do_cmd(self, "hmset", hashname, ...)
end
function _M.array_to_hash(self, t)
local n = #t
-- print("n = ", n)
local h = new_tab(0, n / 2)
for i = 1, n, 2 do
h[t[i]] = t[i + 1]
end
return h
end
function _M.add_commands(...)
local cmds = {...}
for i = 1, #cmds do
local cmd = cmds[i]
_M[cmd] =
function (self, ...)
return _do_cmd(self, cmd, ...)
end
end
end
return _M

View File

@ -0,0 +1,825 @@
--[[
Stupid test codes for ledis.lua. Running based on openresty configuration file.
Remember setting up your `lua_package_path`.
location = /test {
default_type "application/json";
content_by_lua_file /path/to/ledis_test.lua;
}
Then use `curl` or other http clients to see the output.
--]]
local ledis = require "ledis"
local lds = ledis:new()
lds:set_timeout(1000)
-- connect
local ok, err = lds:connect("127.0.0.1", "6380")
if not ok then
ngx.say("failed to connect:", err)
return
end
function cleanUp()
lds:del("mykey", "key1", "key2", "key3", "non_exists_key")
lds:hmclear("myhash", "myhash1", "myhash2")
lds:lclear("mylist")
lds:zmclear("myset", "myset1", "myset2")
return
end
cleanUp()
ngx.say("======================= K/V =====================\n")
--[[KV]]--
-- decr
local res, err = lds:decr("mykey")
if not res then
ngx.say("failed to decr:", err)
return
end
ngx.say("DECR, should be: -1 <=> ", res)
lds:del("mykey")
-- decrby
local res, err = lds:decrby("mykey", 10)
if not res then
ngx.say("failed to decrby:", err)
return
end
ngx.say("DECRBY, should be: -10 <=> ", res)
lds:del("mykey")
-- del
lds:set("key1", "foo")
lds:set("key2", "bar")
local res, err = lds:del("key1", "key2")
if not res then
ngx.say("failed to del:", err)
return
end
ngx.say("DEL, should be: 2 <=> 2")
--exists
lds:set("mykey", "foo")
res, err = lds:exists("mykey")
if not res then
ngx.say("failed to exists: ", err)
return
end
ngx.say("EXISTS, should be 1 <=>", res)
lds:del("mykey")
res, err = lds:exists("non_exists_key")
if not res then
ngx.say("failed to exists: ", err)
return
end
ngx.say("EXISTS, should be 0 <=>", res)
lds:del("non_exists_key")
-- get
lds:set("mykey", "foo")
res, err = lds:get("mykey")
if not res then
ngx.say("failed to get: ", err)
return
end
ngx.say("GET, should be foo <=> ", res)
lds:del("mykey")
-- getset
lds:set("mykey", "foo")
res, err = lds:getset("mykey", "bar")
if not res then
ngx.say("failed to getset ", err)
return
end
ngx.say("GETSET, should be foo <=> ", res)
res, err = lds:get("mykey")
ngx.say("GET, should be bar <=>", res)
lds:del("mykey")
-- incr
lds:set("mykey", "10")
res, err = lds:incr("mykey")
if not res then
ngx.say("failed to incr ", err)
return
end
ngx.say("INCR should be 11 <=>", res)
lds:del("mykey")
-- incrby
lds:set("mykey", "10")
res, err = lds:incrby("mykey", 10)
if not res then
ngx.say("failed to incrby ", err)
return
end
ngx.say("INCRBY should be 20 <=>", res)
lds:del("mykey")
-- mget
lds:set("key1", "foo")
lds:set("key2", "bar")
res, err = lds:mget("key1", "key2")
if not res then
ngx.say("failed to mget ", err)
return
end
ngx.say("MGET should be foobar <=>", res)
lds:del("key1", "key2")
-- mset
res, err = lds:mset("key1", "foo", "key2", "bar")
if not res then
ngx.say("failed to command ", err)
return
end
ngx.say("MSET should be OK <=>", res)
lds:del("key1", "key2")
-- set
ok, err = lds:set("mykey", "foo")
if not ok then
ngx.say("failed to set: ", err)
return
end
ngx.say("SET, should be OK <=>", ok)
lds:del("mykey")
-- setnx
res, err = lds:setnx("mykey", "foo")
if not res then
ngx.say("failed to setnx ", err)
return
end
ngx.say("setnx should be 1 <=>", res)
res, err = lds:setnx("mykey", "foo")
ngx.say("setnx should be 0 <=>", res)
lds:del("mykey")
-- expire
lds:set("mykey", "foo")
res, err = lds:expire("mykey", 60)
if not res then
ngx.say("failed to expire ", err)
return
end
ngx.say("EXPIRE should be 1 <=> ", res)
lds:del("mykey")
-- expireat
lds:set("mykey", "foo")
res, err = lds:expire("mykey", 14366666666)
if not res then
ngx.say("failed to expireat", err)
return
end
ngx.say("EXPIREAT 1 <=>", res)
lds:del("mykey")
-- ttl
lds:set("mykey", "foo")
lds:expire("mykey", 100)
res, err = lds:ttl("mykey")
if not res then
ngx.say("failed to ttl ", err)
return
end
if not (0 < res and res <= 100) then
ngx.say("failed to ttl")
return
end
ngx.say("TTL ", res)
lds:del("mykey")
-- persist
lds:set("mykey", "foo")
lds:expire("mykey", 100)
res, err = lds:persist("mykey")
if not res then
ngx.say("failed to persist", err)
return
end
ngx.say("PERSIST should be 1 <=>", res)
lds:del("mykey")
ngx.say("\n=================== HASH =====================\n")
-- [[ HASH ]]
-- hdel
res, err = lds:hset("myhash", "field", "foo")
if not res then
ngx.say("failed to HDEL", err)
return
end
ngx.say("HDEL should be 1 <=>", res)
lds:hclear("myhash")
-- hexists
lds:hset("myhash", "field", "foo")
res, err = lds:hexists("myhash", "field")
if not res then
ngx.say("failed to HEXISTS", err)
return
end
ngx.say("HEXISTS should be 1 <=>", res)
lds:hclear("myhash")
-- hget
lds:hset("myhash", "field", "foo")
res, err = lds:hget("myhash", "field")
if not res then
ngx.say("failed to HGET ", err)
return
end
ngx.say("HGET should be foo <=>", res)
lds:hclear("myhash")
-- hgetall
lds:hmset("myhash", "field1", "foo", "field2", "bar")
res, err = lds:hgetall("myhash")
if not res then
ngx.say("failed to HGETALL ", err)
return
end
ngx.say("HGETALL should be field1foofield2bar <=>", res)
lds:hclear("myhash")
-- hincrby
res, err = lds:hincrby("myhash", "field", 1)
if not res then
ngx.say("failed to HINCRBY ", err)
return
end
ngx.say("HINCRBY should be 1 <=>", res)
lds:hclear("myhash")
-- hkeys
lds:hmset("myhash", "field1", "foo", "field2", "bar")
res, err = lds:hkeys("myhash")
if not res then
ngx.say("failed to HKEYS", err)
return
end
ngx.say("HKEYS should be field1field2 <=> ", res)
lds:hclear("myhash")
-- hlen
lds:hset("myhash", "field", "foo")
res, err = lds:hlen("myhash")
if not res then
ngx.say("failed to HLEN ", err)
return
end
ngx.say("HLEN should be 1 <=>", res)
lds:hclear("myhash")
-- hmget
lds:hmset("myhash", "field1", "foo", "field2", "bar")
res, err = lds:hmget("myhash", "field1", "field2")
if not res then
ngx.say("failed to HMGET", err)
return
end
ngx.say("HMGET should be foobar <=>", res)
lds:hclear("myhash")
-- hmset
res, err = lds:hmset("myhash", "field1", "foo", "field2", "bar")
if not res then
ngx.say("failed to HMSET ", err)
return
end
local l = lds:hlen("myhash")
if l == 2 then
ngx.say("HMSET tested !")
else
ngx.say("HMSET failed")
end
res, err = lds:hclear("myhash")
-- hset
res, err = lds:hset("myhash", "field", "foo")
if not res then
ngx.say("failed to HSET", err)
return
end
ngx.say("HSET should be 1 <=> ", res)
lds:hclear("myhash")
-- hvals
lds:hset("myhash", "field", "foo")
res, err = lds:hvals("myhash")
if not res then
ngx.say("failed to HVALS", err)
return
end
ngx.say("HVALS should be foo <=>", res)
lds:hclear("myhash")
-- hclear
lds:hmset("myhash", "field1", "foo", "field2", "bar")
res, err = lds:hclear("myhash")
if not res then
ngx.say("failed to HCLEAR", err)
return
end
ngx.say("HCLEAR should be 2 <=>", res)
-- hmclear
lds:hset("myhash1", "field1", "foo")
lds:hset("myhash2", "field2", "bar")
res, err = lds:hmclear("myhash1", "myhash2")
if not res then
ngx.say("failed to HMCLEAR ", err)
return
end
ngx.say("HMCLEAR should be 2 <=>", res)
-- hexpire
lds:hset("myhash", "field", "foo")
res, err = lds:hexpire("myhash", 100)
if not res then
ngx.say("failed to HEXPIRE", err)
return
end
ngx.say("HEXPIRE should be 1 <=>", res)
lds:hclear("myhash")
-- hexpireat
lds:hset("myhash", "field", "foo")
res, err = lds:hexpireat("myhash", 14366666666)
if not res then
ngx.say("failed to HEXPIREAT", err)
return
end
ngx.say("HEXPIREAT should be 1 <=>", res)
lds:hclear("myhash")
-- hpersist
lds:hset("myhash", "field", "foo")
lds:hexpire("myhash", 100)
res, err = lds:hpersist("myhash")
if not res then
ngx.say("failed to hpersist", err)
return
end
ngx.say("HPERSIST should be 1 <=>", res)
lds:hclear("myhash")
--httl
lds:hset("myhash", "field", "foo")
lds:hexpire("myhash", 100)
res, err = lds:httl("myhash")
if not res then
ngx.say("failed to HTTL ", err)
return
end
ngx.say("HTTL value: ", res)
lds:hclear("myhash")
ngx.say("\n================== LIST ====================\n")
-- [[ LIST ]]
-- lindex
lds:rpush("mylist", "one", "two", "three")
res, err = lds:lindex("mylist", 0)
if not res then
ngx.say("failed to LINDEX ", err)
return
end
ngx.say("LINDEX should be one <=>", res)
lds:lclear("mylist")
-- llen
lds:rpush("mylist", "foo", "bar")
res, err = lds:llen("mylist")
if not res then
ngx.say("failed to LLEN ", err)
return
end
ngx.say("LLEN should be 2 <=>", res)
lds:lclear("mylist")
-- lpop
lds:rpush("mylist", "one", "two", "three")
res, err = lds:lpop("mylist")
if not res then
ngx.say("failed to LPOP ", err)
return
end
ngx.say("LPOP should be one <=>", res)
lds:lclear("mylist")
-- lrange
lds:rpush("mylist", "one", "two", "three")
res, err = lds:lrange("mylist", 0, 0)
if not res then
ngx.say("failed to one ", err)
return
end
ngx.say("LRANGE should be one <=>", res)
lds:lclear("mylist")
-- lpush
res, err = lds:lpush("mylist", "one", "two")
if not res then
ngx.say("failed to LPUSH ", err)
return
end
ngx.say("LPUSH should be 2 <=>", res)
lds:lclear("mylist")
-- rpop
lds:rpush("mylist", "one", "two")
res, err = lds:rpop("mylist")
if not res then
ngx.say("failed to RPOP ", err)
return
end
ngx.say("RPOP should be two <=>", res)
lds:lclear("mylist")
-- rpush
res, err = lds:rpush("mylist", "one", "two")
if not res then
ngx.say("failed to RPUSH ", err)
return
end
ngx.say("RPUSH should be 2 <=>", res)
lds:lclear("mylist")
-- lclear
lds:rpush("mylist", "one", "two")
res, err = lds:lclear("mylist")
if not res then
ngx.say("failed to LCLEAR ", err)
return
end
ngx.say("LCLEAR should be 2 <=>", res)
-- lexpire
lds:rpush("mylist", "one")
res, err = lds:lexpire("mylist", 100)
if not res then
ngx.say("failed to LEXPIRE", err)
return
end
ngx.say("LEXPIRE should be 1 <=>", res)
lds:lclear("mylist")
-- lexpireat
lds:rpush("mylist", "one")
res, err = lds:lexpireat("mylist", 14366666666)
if not res then
ngx.say("failed to LEXPIREAT", err)
return
end
ngx.say("LEXPIREAT should be 1 <=>", res)
lds:lclear("mylist")
-- lpersist
lds:rpush("mylist", "one", "two")
lds:lexpire("mylist", 100)
res, err = lds:lpersist("mylist")
if not res then
ngx.say("failed to lpersist", err)
return
end
ngx.say("LPERSIST should be 1 <=>", res)
lds:hclear("mylist")
--lttl
lds:rpush("mylist", "field", "foo")
lds:lexpire("mylist", 100)
res, err = lds:lttl("mylist")
if not res then
ngx.say("failed to LTTL ", err)
return
end
ngx.say("LTTL value: ", res)
lds:lclear("mylist")
ngx.say("\n==================== ZSET =====================\n")
-- [[ ZSET ]]
-- zadd
res, err = lds:zadd("myset", 1, "one")
if not res then
ngx.say("failed to ZADD ", err)
return
end
ngx.say("ZADD should be 1 <=>", res)
lds:zclear("myset")
-- zcard
lds:zadd("myset", 1, "one", 2, "two")
res, err = lds:zcard("myset")
if not res then
ngx.say("failed to ZCARD ", err)
return
end
ngx.say("ZCARD should be 2 <=>", res)
lds:zclear("myset")
-- zcount
lds:zadd("myset", 1, "one", 2, "two")
res, err = lds:zcount("myset", "-inf", "+inf")
if not res then
ngx.say("failed to ZCOUNT ", err)
return
end
ngx.say("ZCOUNT should be 2 <=>", res)
lds:zclear("myset")
-- zincrby
lds:zadd("myset", 1, "one")
res, err = lds:zincrby("myset", 2, "one")
if not res then
ngx.say("failed to ZINCRBY ", err)
return
end
ngx.say("ZINCRBY should be 3 <=>", res)
lds:zclear("myset")
-- zrange
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrange("myset", 0, -1, "WITHSCORES")
if not res then
ngx.say("failed to ZRANGE ", err)
return
end
ngx.say("ZRANGE should be one1two2three3<=>", res)
lds:zclear("myset")
-- zrangebyscore
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrangebyscore("myset", 1, 2)
if not res then
ngx.say("failed to ZRANGEBYSCORE ", err)
return
end
ngx.say("ZRANGEBYSCORE should be onetwo <=>", res)
lds:zclear("myset")
-- zrank
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrank("myset", "three")
if not res then
ngx.say("failed to ZRANK ", err)
return
end
ngx.say("ZRANK should be 2 <=>", res)
lds:zclear("myset")
-- zrem
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrem("myset", "two", "three")
if not res then
ngx.say("failed to ZREM ", err)
return
end
ngx.say("ZREM should be 2 <=>", res)
lds:zclear("myset")
-- zremrangebyrank
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err= lds:zremrangebyrank("myset", 0, 2)
if not res then
ngx.say("failed to ZREMRANGEBYRANK ", err)
return
end
ngx.say("ZREMRANGEBYRANK should be 3 <=>", res)
lds:zclear("myset")
-- zremrangebyscore
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zremrangebyscore("myset", 0, 2)
if not res then
ngx.say("failed to ZREMRANGEBYSCORE ", err)
return
end
ngx.say("zremrangebyscore should be 2 <=>", res)
lds:zclear("myset")
-- zrevrange
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrevrange("myset", 0, -1)
if not res then
ngx.say("failed to ZREVRANGE ", err)
return
end
ngx.say("ZREVRANGE should be threetwoone <=>", res)
lds:zclear("myset")
-- zrevrangebyscore
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zrevrangebyscore("myset", "+inf", "-inf")
if not res then
ngx.say("failed to ZREVRANGEBYSCORE ", err)
return
end
ngx.say("ZREVRANGEBYSCORE should be threetwoone <=>", res)
lds:zclear("myset")
-- zscore
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zscore("myset", "two")
if not res then
ngx.say("failed to ZSCORE ", err)
return
end
ngx.say("ZSCORE should be 2 <=>", res)
lds.zclear("myset")
-- zclear
lds:zadd("myset", 1, "one", 2, "two", 3, "three")
res, err = lds:zclear("myset")
if not res then
ngx.say("failed to ZCLEAR ", err)
return
end
ngx.say("ZCLEAR should be 3 <=>", res)
-- zmclear
lds:zadd("myset1", 1, "one", 2, "two", 3, "three")
lds:zadd("myset2", 1, "one", 2, "two", 3, "three")
res, err = lds:zmclear("myset1", "myset2")
if not res then
ngx.say("failed to ZMCLEAR ", err)
return
end
ngx.say("ZMCLEAR should be 2 <=>", res)
-- zexpire
lds:zadd("myset", 1, "one")
res, err = lds:zexpire("myset", 60)
if not res then
ngx.say("failed to zexpire ", err)
return
end
ngx.say("ZEXPIRE should be 1 <=> ", res)
lds:zclear("myset")
-- zexpireat
lds:zadd("myset", 1, "one")
res, err = lds:zexpire("myset", 14366666666)
if not res then
ngx.say("failed to zexpireat", err)
return
end
ngx.say("ZEXPIREAT 1 <=>", res)
lds:zclear("myset")
-- zttl
lds:zadd("myset", 1, "one")
lds:zexpire("myset", 100)
res, err = lds:zttl("myset")
if not res then
ngx.say("failed to zttl ", err)
return
end
if not (0 < res and res <= 100) then
ngx.say("failed to zttl")
return
end
ngx.say("ZTTL ", res)
lds:zclear("myset")
-- zpersist
lds:zadd("myset", 1, "one")
lds:zexpire("myset", 100)
res, err = lds:zpersist("myset")
if not res then
ngx.say("failed to zpersist", err)
return
end
ngx.say("ZPERSIST should be 1 <=>", res)
lds:zclear("myset")
ngx.say("\n===================== SERVER INFO ==============\n")
-- [[ SERVER INFO ]]
-- ping
res, err = lds:ping()
if not res then
ngx.say("failed to PING ", err)
return
end
ngx.say("PING should be PONG <=>", res)
-- echo
res, err = lds:echo("hello, lua")
if not res then
ngx.say("failed to ECHO ", err)
return
end
ngx.say("ECHO should be hello, lua <=>", res)
-- select
res, err = lds:select(5)
if not res then
ngx.say("failed to SELECT ", err)
return
end
ngx.say("SELECT should be OK <=>", res)

View File

@ -0,0 +1,16 @@
Copyright and License
=====================
This module is licensed under the BSD license.
Copyright (C) 2012-2014, by Yichun Zhang (agentzh) <agentzh@gmail.com>, CloudFlare Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.