forked from mirror/ledisdb
Merge branch 'client-feature' into develop
This commit is contained in:
commit
961b4f393d
|
@ -1 +1,3 @@
|
|||
build
|
||||
build
|
||||
*.pyc
|
||||
.DS_Store
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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."
|
|
@ -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}
|
|
@ -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`
|
||||
|
|
@ -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',
|
||||
]
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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.
|
|
@ -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',
|
||||
]
|
||||
)
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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'))
|
|
@ -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
|
|
@ -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)
|
|
@ -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.
|
Loading…
Reference in New Issue