├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── doc ├── Makefile └── source │ ├── conf.py │ └── index.rst ├── ledis ├── __init__.py ├── _compat.py ├── client.py ├── connection.py ├── exceptions.py └── utils.py ├── redis-py_license ├── setup.py └── tests ├── __init__.py ├── all.sh ├── test_cmd_bit.py ├── test_cmd_hash.py ├── test_cmd_kv.py ├── test_cmd_list.py ├── test_cmd_script.py ├── test_cmd_set.py ├── test_cmd_zset.py ├── test_others.py ├── test_tx.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 siddontang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include redis-py_license 3 | exclude tests/__pycache__ 4 | recursive-include tests * 5 | recursive-exclude tests *.pyc 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | 4 | test: 5 | sh tests/all.sh 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### UNMAINTAINED 2 | 3 | #ledis-py 4 | 5 | The Python client to the [LedisDB](https://github.com/siddontang/ledisdb) key-value store. 6 | 7 | 8 | ##Installation 9 | 10 | 11 | ledis-py requires a running ledisdb server. See [ledisdb guide](https://github.com/siddontang/ledisdb#build-and-install) for installation instructions. 12 | 13 | To install ledis-py, simply using `pip`(recommended): 14 | 15 | ``` 16 | $ sudo pip install ledis 17 | ``` 18 | 19 | or alternatively, using `easy_install`: 20 | 21 | ``` 22 | $ sudo easy_install ledis 23 | ``` 24 | 25 | or install from the source: 26 | 27 | ``` 28 | $ sudo python setup.py install 29 | ``` 30 | 31 | ##Getting Started 32 | 33 | ``` 34 | >>> import ledis 35 | >>> l = ledis.Ledis(host='localhost', port=6380, db=0) 36 | >>> l.set('foo', 'bar') 37 | True 38 | >>> l.get('foo') 39 | 'bar' 40 | >>> 41 | ``` 42 | 43 | ## API Reference 44 | 45 | For full API reference, please visit [rtfd](http://ledis-py.readthedocs.org/). 46 | 47 | 48 | ## Connection 49 | 50 | ### Connection Pools 51 | 52 | Behind the scenes, ledis-py uses a connection pool to manage connections to a Ledis server. By default, each Ledis instance you create will in turn create its own connection pool. You can override this behavior and use an existing connection pool by passing an already created connection pool instance to the connection_pool argument of the Ledis class. You may choose to do this in order to implement client side sharding or have finer grain control of how connections are managed. 53 | 54 | ``` 55 | >>> pool = ledis.ConnectionPool(host='localhost', port=6380, db=0) 56 | >>> l = ledis.Ledis(connection_pool=pool) 57 | ``` 58 | 59 | ### Connections 60 | 61 | ConnectionPools manage a set of Connection instances. ledis-py ships with two types of Connections. The default, Connection, is a normal TCP socket based connection. The UnixDomainSocketConnection allows for clients running on the same device as the server to connect via a unix domain socket. To use a UnixDomainSocketConnection connection, simply pass the unix_socket_path argument, which is a string to the unix domain socket file. Additionally, make sure the unixsocket parameter is defined in your `ledis.json` file. e.g.: 62 | 63 | ``` 64 | { 65 | "addr": "/tmp/ledis.sock", 66 | ... 67 | } 68 | ``` 69 | 70 | ``` 71 | >>> l = ledis.Ledis(unix_socket_path='/tmp/ledis.sock') 72 | ``` 73 | 74 | You can create your own Connection subclasses as well. This may be useful if you want to control the socket behavior within an async framework. To instantiate a client class using your own connection, you need to create a connection pool, passing your class to the connection_class argument. Other keyword parameters your pass to the pool will be passed to the class specified during initialization. 75 | 76 | ``` 77 | >>> pool = ledis.ConnectionPool(connection_class=YourConnectionClass, 78 | your_arg='...', ...) 79 | ``` 80 | 81 | e.g.: 82 | 83 | ``` 84 | >>> from ledis import UnixDomainSocketConnection 85 | >>> pool = ledis.ConnectionPool(connection_class=UnixDomainSocketConnection, path='/tmp/ledis.sock') 86 | ``` 87 | 88 | ## Response Callbacks 89 | 90 | The client class uses a set of callbacks to cast Ledis responses to the appropriate Python type. There are a number of these callbacks defined on the Ledis client class in a dictionary called RESPONSE_CALLBACKS. 91 | 92 | Custom callbacks can be added on a per-instance basis using the `set_response_callback` method. This method accepts two arguments: a command name and the callback. Callbacks added in this manner are only valid on the instance the callback is added to. If you want to define or override a callback globally, you should make a subclass of the Ledis client and add your callback to its RESPONSE_CALLBACKS class dictionary. 93 | 94 | Response callbacks take at least one parameter: the response from the Ledis server. Keyword arguments may also be accepted in order to further control how to interpret the response. These keyword arguments are specified during the command's call to execute_command. The ZRANGE implementation demonstrates the use of response callback keyword arguments with its "withscores" argument. 95 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ledis-py.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ledis-py.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ledis-py" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ledis-py" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ledis-py documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 8 20:03:17 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.append(os.path.abspath('../..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'ledis-py' 44 | copyright = u'2014, Andy McCurdy' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'nature' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'ledis-pydoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'ledis-py.tex', u'ledis-py Documentation', 187 | u'Andy McCurdy', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'ledis-py', u'ledis-py Documentation', 217 | [u'Andy McCurdy'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'ledis-py', u'ledis-py Documentation', 231 | u'Andy McCurdy', 'ledis-py', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | 244 | 245 | # Example configuration for intersphinx: refer to the Python standard library. 246 | intersphinx_mapping = {'http://docs.python.org/': None} 247 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ledis-py documentation master file, created by 2 | sphinx-quickstart on Tue Jul 8 20:03:17 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ledis-py's documentation! 7 | ==================================== 8 | 9 | 10 | API Reference 11 | ------------- 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | .. automodule:: ledis 17 | :members: 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /ledis/__init__.py: -------------------------------------------------------------------------------- 1 | from ledis.client import Ledis 2 | from ledis.connection import ( 3 | BlockingConnectionPool, 4 | ConnectionPool, 5 | Connection, 6 | UnixDomainSocketConnection 7 | ) 8 | from ledis.utils import from_url 9 | from ledis.exceptions import ( 10 | ConnectionError, 11 | BusyLoadingError, 12 | DataError, 13 | InvalidResponse, 14 | LedisError, 15 | ResponseError, 16 | ) 17 | 18 | 19 | __version__ = '0.0.1' 20 | VERSION = tuple(map(int, __version__.split('.'))) 21 | 22 | __all__ = [ 23 | 'Ledis', 'ConnectionPool', 'BlockingConnectionPool', 24 | 'Connection', 'UnixDomainSocketConnection', 25 | 'LedisError', 'ConnectionError', 'ResponseError', 26 | 'InvalidResponse', 'DataError', 'from_url', 'BusyLoadingError', 27 | ] 28 | -------------------------------------------------------------------------------- /ledis/_compat.py: -------------------------------------------------------------------------------- 1 | """Internal module for Python 2 backwards compatibility.""" 2 | import sys 3 | 4 | 5 | if sys.version_info[0] < 3: 6 | from urlparse import parse_qs, urlparse 7 | from itertools import imap, izip 8 | from string import letters as ascii_letters 9 | from Queue import Queue 10 | try: 11 | from cStringIO import StringIO as BytesIO 12 | except ImportError: 13 | from StringIO import StringIO as BytesIO 14 | 15 | iteritems = lambda x: x.iteritems() 16 | iterkeys = lambda x: x.iterkeys() 17 | itervalues = lambda x: x.itervalues() 18 | nativestr = lambda x: \ 19 | x if isinstance(x, str) else x.encode('utf-8', 'replace') 20 | u = lambda x: x.decode() 21 | b = lambda x: x 22 | next = lambda x: x.next() 23 | byte_to_chr = lambda x: x 24 | unichr = unichr 25 | xrange = xrange 26 | basestring = basestring 27 | unicode = unicode 28 | bytes = str 29 | long = long 30 | else: 31 | from urllib.parse import parse_qs, urlparse 32 | from io import BytesIO 33 | from string import ascii_letters 34 | from queue import Queue 35 | 36 | iteritems = lambda x: iter(x.items()) 37 | iterkeys = lambda x: iter(x.keys()) 38 | itervalues = lambda x: iter(x.values()) 39 | byte_to_chr = lambda x: chr(x) 40 | nativestr = lambda x: \ 41 | x if isinstance(x, str) else x.decode('utf-8', 'replace') 42 | u = lambda x: x 43 | b = lambda x: x.encode('iso-8859-1') if not isinstance(x, bytes) else x 44 | next = next 45 | unichr = chr 46 | imap = map 47 | izip = zip 48 | xrange = range 49 | basestring = str 50 | unicode = str 51 | bytes = bytes 52 | long = int 53 | 54 | try: # Python 3 55 | from queue import LifoQueue, Empty, Full 56 | except ImportError: 57 | from Queue import Empty, Full 58 | try: # Python 2.6 - 2.7 59 | from Queue import LifoQueue 60 | except ImportError: # Python 2.5 61 | from Queue import Queue 62 | # From the Python 2.7 lib. Python 2.5 already extracted the core 63 | # methods to aid implementating different queue organisations. 64 | 65 | class LifoQueue(Queue): 66 | "Override queue methods to implement a last-in first-out queue." 67 | 68 | def _init(self, maxsize): 69 | self.maxsize = maxsize 70 | self.queue = [] 71 | 72 | def _qsize(self, len=len): 73 | return len(self.queue) 74 | 75 | def _put(self, item): 76 | self.queue.append(item) 77 | 78 | def _get(self): 79 | return self.queue.pop() 80 | -------------------------------------------------------------------------------- /ledis/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import datetime 3 | import time as mod_time 4 | from itertools import chain, starmap 5 | from ledis._compat import (b, izip, imap, iteritems, 6 | basestring, long, nativestr, bytes) 7 | from ledis.connection import ConnectionPool, UnixDomainSocketConnection, Token 8 | from ledis.exceptions import ( 9 | ConnectionError, 10 | DataError, 11 | LedisError, 12 | ResponseError, 13 | TxNotBeginError 14 | ) 15 | 16 | SYM_EMPTY = b('') 17 | 18 | 19 | def list_or_args(keys, args): 20 | # returns a single list combining keys and args 21 | try: 22 | iter(keys) 23 | # a string or bytes instance can be iterated, but indicates 24 | # keys wasn't passed as a list 25 | if isinstance(keys, (basestring, bytes)): 26 | keys = [keys] 27 | except TypeError: 28 | keys = [keys] 29 | if args: 30 | keys.extend(args) 31 | return keys 32 | 33 | 34 | def string_keys_to_dict(key_string, callback): 35 | return dict.fromkeys(key_string.split(), callback) 36 | 37 | 38 | def dict_merge(*dicts): 39 | merged = {} 40 | [merged.update(d) for d in dicts] 41 | return merged 42 | 43 | 44 | def pairs_to_dict(response): 45 | "Create a dict given a list of key/value pairs" 46 | it = iter(response) 47 | return dict(izip(it, it)) 48 | 49 | 50 | def zset_score_pairs(response, **options): 51 | """ 52 | If ``withscores`` is specified in the options, return the response as 53 | a list of (value, score) pairs 54 | """ 55 | if not response or not options['withscores']: 56 | return response 57 | it = iter(response) 58 | return list(izip(it, imap(int, it))) 59 | 60 | 61 | def int_or_none(response): 62 | if response is None: 63 | return None 64 | return int(response) 65 | 66 | 67 | def parse_info(response): 68 | 69 | info = {} 70 | response = nativestr(response) 71 | 72 | def get_value(value): 73 | if ',' not in value or '=' not in value: 74 | try: 75 | if '.' in value: 76 | return float(value) 77 | else: 78 | return int(value) 79 | except ValueError: 80 | return value 81 | 82 | for line in response.splitlines(): 83 | if line and not line.startswith('#'): 84 | if line.find(':') != -1: 85 | key, value = line.split(':', 1) 86 | info[key] = get_value(value) 87 | 88 | return info 89 | 90 | # def parse_lscan(response, ) 91 | 92 | class Ledis(object): 93 | """ 94 | Implementation of the Redis protocol. 95 | 96 | This abstract class provides a Python interface to all LedisDB commands 97 | and an implementation of the Redis protocol. 98 | 99 | Connection and Pipeline derive from this, implementing how 100 | the commands are sent and received to the Ledis server 101 | """ 102 | RESPONSE_CALLBACKS = dict_merge( 103 | string_keys_to_dict( 104 | 'EXISTS HEXISTS SISMEMBER HMSET SETNX' 105 | 'PERSIST HPERSIST LPERSIST ZPERSIST SPERSIST BPERSIST' 106 | 'EXPIRE LEXPIRE HEXPIRE SEXPIRE ZEXPIRE BEXPIRE' 107 | 'EXPIREAT LBEXPIREAT HEXPIREAT SEXPIREAT ZEXPIREAT BEXPIREAT', 108 | bool 109 | ), 110 | string_keys_to_dict( 111 | 'DECRBY DEL HDEL HLEN INCRBY LLEN ZADD ZCARD ZREM' 112 | 'ZREMRANGEBYRANK ZREMRANGEBYSCORE LMCLEAR HMCLEAR' 113 | 'ZMCLEAR BCOUNT BGETBIT BSETBIT BOPT BMSETBIT' 114 | 'SADD SCARD SDIFFSTORE SINTERSTORE SUNIONSTORE SREM' 115 | 'SCLEAR SMLEAR BDELETE', 116 | int 117 | ), 118 | string_keys_to_dict( 119 | 'LPUSH RPUSH', 120 | lambda r: isinstance(r, long) and r or nativestr(r) == 'OK' 121 | ), 122 | string_keys_to_dict( 123 | 'MSET SELECT', 124 | lambda r: nativestr(r) == 'OK' 125 | ), 126 | string_keys_to_dict( 127 | 'SDIFF SINTER SMEMBERS SUNION', 128 | lambda r: r and set(r) or set() 129 | ), 130 | string_keys_to_dict( 131 | 'ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE', 132 | zset_score_pairs 133 | ), 134 | string_keys_to_dict('ZRANK ZREVRANK ZSCORE ZINCRBY', int_or_none), 135 | { 136 | 'HGETALL': lambda r: r and pairs_to_dict(r) or {}, 137 | 'PING': lambda r: nativestr(r) == 'PONG', 138 | 'SET': lambda r: r and nativestr(r) == 'OK', 139 | 'INFO': parse_info, 140 | } 141 | 142 | 143 | ) 144 | 145 | @classmethod 146 | def from_url(cls, url, db=None, **kwargs): 147 | """ 148 | Return a Ledis client object configured from the given URL. 149 | 150 | For example:: 151 | 152 | ledis://localhost:6380/0 153 | unix:///path/to/socket.sock?db=0 154 | 155 | There are several ways to specify a database number. The parse function 156 | will return the first specified option: 157 | 1. A ``db`` querystring option, e.g. ledis://localhost?db=0 158 | 2. If using the ledis:// scheme, the path argument of the url, e.g. 159 | ledis://localhost/0 160 | 3. The ``db`` argument to this function. 161 | 162 | If none of these options are specified, db=0 is used. 163 | 164 | Any additional querystring arguments and keyword arguments will be 165 | passed along to the ConnectionPool class's initializer. In the case 166 | of conflicting arguments, querystring arguments always win. 167 | """ 168 | connection_pool = ConnectionPool.from_url(url, db=db, **kwargs) 169 | return cls(connection_pool=connection_pool) 170 | 171 | def __init__(self, host='localhost', port=6380, 172 | db=0, socket_timeout=None, 173 | connection_pool=None, charset='utf-8', 174 | errors='strict', decode_responses=False, 175 | unix_socket_path=None): 176 | if not connection_pool: 177 | kwargs = { 178 | 'db': db, 179 | 'socket_timeout': socket_timeout, 180 | 'encoding': charset, 181 | 'encoding_errors': errors, 182 | 'decode_responses': decode_responses, 183 | } 184 | # based on input, setup appropriate connection args 185 | if unix_socket_path: 186 | kwargs.update({ 187 | 'path': unix_socket_path, 188 | 'connection_class': UnixDomainSocketConnection 189 | }) 190 | else: 191 | kwargs.update({ 192 | 'host': host, 193 | 'port': port 194 | }) 195 | connection_pool = ConnectionPool(**kwargs) 196 | self.connection_pool = connection_pool 197 | self.response_callbacks = self.__class__.RESPONSE_CALLBACKS.copy() 198 | 199 | def set_response_callback(self, command, callback): 200 | "Set a custom Response Callback" 201 | self.response_callbacks[command] = callback 202 | 203 | def tx(self): 204 | return Transaction( 205 | self.connection_pool, 206 | self.response_callbacks) 207 | 208 | #### COMMAND EXECUTION AND PROTOCOL PARSING #### 209 | 210 | def execute_command(self, *args, **options): 211 | "Execute a command and return a parsed response" 212 | pool = self.connection_pool 213 | command_name = args[0] 214 | connection = pool.get_connection(command_name, **options) 215 | try: 216 | connection.send_command(*args) 217 | return self.parse_response(connection, command_name, **options) 218 | except ConnectionError: 219 | connection.disconnect() 220 | connection.send_command(*args) 221 | return self.parse_response(connection, command_name, **options) 222 | finally: 223 | pool.release(connection) 224 | 225 | def parse_response(self, connection, command_name, **options): 226 | "Parses a response from the Ledis server" 227 | response = connection.read_response() 228 | if command_name in self.response_callbacks: 229 | return self.response_callbacks[command_name](response, **options) 230 | return response 231 | 232 | #### SERVER INFORMATION #### 233 | def echo(self, value): 234 | "Echo the string back from the server" 235 | return self.execute_command('ECHO', value) 236 | 237 | def ping(self): 238 | "Ping the Ledis server" 239 | return self.execute_command('PING') 240 | 241 | def info(self, section=None): 242 | """ 243 | Return 244 | """ 245 | 246 | if section is None: 247 | return self.execute_command("INFO") 248 | else: 249 | return self.execute_command('INFO', section) 250 | 251 | def flushall(self): 252 | return self.execute_command('FLUSHALL') 253 | 254 | def flushdb(self): 255 | return self.execute_command('FLUSHDB') 256 | 257 | 258 | #### BASIC KEY COMMANDS #### 259 | def decr(self, name, amount=1): 260 | """ 261 | Decrements the value of ``key`` by ``amount``. If no key exists, 262 | the value will be initialized as 0 - ``amount`` 263 | """ 264 | return self.execute_command('DECRBY', name, amount) 265 | 266 | def decrby(self, name, amount=1): 267 | """ 268 | Decrements the value of ``key`` by ``amount``. If no key exists, 269 | the value will be initialized as 0 - ``amount`` 270 | """ 271 | return self.decr(name, amount) 272 | 273 | def delete(self, *names): 274 | "Delete one or more keys specified by ``names``" 275 | return self.execute_command('DEL', *names) 276 | 277 | def exists(self, name): 278 | "Returns a boolean indicating whether key ``name`` exists" 279 | return self.execute_command('EXISTS', name) 280 | 281 | def expire(self, name, time): 282 | """ 283 | Set an expire flag on key ``name`` for ``time`` seconds. ``time`` 284 | can be represented by an integer or a Python timedelta object. 285 | """ 286 | if isinstance(time, datetime.timedelta): 287 | time = time.seconds + time.days * 24 * 3600 288 | return self.execute_command('EXPIRE', name, time) 289 | 290 | def expireat(self, name, when): 291 | """ 292 | Set an expire flag on key ``name``. ``when`` can be represented 293 | as an integer indicating unix time or a Python datetime object. 294 | """ 295 | if isinstance(when, datetime.datetime): 296 | when = int(mod_time.mktime(when.timetuple())) 297 | return self.execute_command('EXPIREAT', name, when) 298 | 299 | def get(self, name): 300 | """ 301 | Return the value at key ``name``, or None if the key doesn't exist 302 | """ 303 | return self.execute_command('GET', name) 304 | 305 | def __getitem__(self, name): 306 | """ 307 | Return the value at key ``name``, raises a KeyError if the key 308 | doesn't exist. 309 | """ 310 | value = self.get(name) 311 | if value: 312 | return value 313 | raise KeyError(name) 314 | 315 | def getset(self, name, value): 316 | """ 317 | Set the value at key ``name`` to ``value`` if key doesn't exist 318 | Return the value at key ``name`` atomically 319 | """ 320 | return self.execute_command('GETSET', name, value) 321 | 322 | def incr(self, name, amount=1): 323 | """ 324 | Increments the value of ``key`` by ``amount``. If no key exists, 325 | the value will be initialized as ``amount`` 326 | """ 327 | return self.execute_command('INCRBY', name, amount) 328 | 329 | def incrby(self, name, amount=1): 330 | """ 331 | Increments the value of ``key`` by ``amount``. If no key exists, 332 | the value will be initialized as ``amount`` 333 | """ 334 | 335 | # An alias for ``incr()``, because it is already implemented 336 | # as INCRBY ledis command. 337 | return self.incr(name, amount) 338 | 339 | def mget(self, keys, *args): 340 | """ 341 | Returns a list of values ordered identically to ``keys`` 342 | """ 343 | args = list_or_args(keys, args) 344 | return self.execute_command('MGET', *args) 345 | 346 | def mset(self, *args, **kwargs): 347 | """ 348 | Sets key/values based on a mapping. Mapping can be supplied as a single 349 | dictionary argument or as kwargs. 350 | """ 351 | if args: 352 | if len(args) != 1 or not isinstance(args[0], dict): 353 | raise LedisError('MSET requires **kwargs or a single dict arg') 354 | kwargs.update(args[0]) 355 | items = [] 356 | for pair in iteritems(kwargs): 357 | items.extend(pair) 358 | return self.execute_command('MSET', *items) 359 | 360 | def set(self, name, value): 361 | """ 362 | Set the value of key ``name`` to ``value``. 363 | """ 364 | pieces = [name, value] 365 | return self.execute_command('SET', *pieces) 366 | 367 | def setnx(self, name, value): 368 | "Set the value of key ``name`` to ``value`` if key doesn't exist" 369 | return self.execute_command('SETNX', name, value) 370 | 371 | def setex(self, name, time, value): 372 | if isinstance(time, datetime.timedelta): 373 | time = time.seconds + time.days * 24 * 3600 374 | return self.execute_command("setex", name, time, value) 375 | 376 | def ttl(self, name): 377 | "Returns the number of seconds until the key ``name`` will expire" 378 | return self.execute_command('TTL', name) 379 | 380 | def persist(self, name): 381 | "Removes an expiration on name" 382 | return self.execute_command('PERSIST', name) 383 | 384 | def xscan(self, ktype="", key="" , match=None, count=10): 385 | pieces = [ktype, key] 386 | if match is not None: 387 | pieces.extend(["MATCH", match]) 388 | 389 | pieces.extend(["COUNT", count]) 390 | 391 | return self.execute_command("XSCAN", *pieces) 392 | 393 | def scan_iter(self, match=None, count=10): 394 | key = "" 395 | while key != "": 396 | key, data = self.scan(key=key, match=match, count=count) 397 | for item in data: 398 | yield item 399 | 400 | #### LIST COMMANDS #### 401 | def lindex(self, name, index): 402 | """ 403 | Return the item from list ``name`` at position ``index`` 404 | 405 | Negative indexes are supported and will return an item at the 406 | end of the list 407 | """ 408 | return self.execute_command('LINDEX', name, index) 409 | 410 | def llen(self, name): 411 | "Return the length of the list ``name``" 412 | return self.execute_command('LLEN', name) 413 | 414 | def lpop(self, name): 415 | "Remove and return the first item of the list ``name``" 416 | return self.execute_command('LPOP', name) 417 | 418 | def lpush(self, name, *values): 419 | "Push ``values`` onto the head of the list ``name``" 420 | return self.execute_command('LPUSH', name, *values) 421 | 422 | def lrange(self, name, start, end): 423 | """ 424 | Return a slice of the list ``name`` between 425 | position ``start`` and ``end`` 426 | 427 | ``start`` and ``end`` can be negative numbers just like 428 | Python slicing notation 429 | """ 430 | return self.execute_command('LRANGE', name, start, end) 431 | 432 | def rpop(self, name): 433 | "Remove and return the last item of the list ``name``" 434 | return self.execute_command('RPOP', name) 435 | 436 | def rpush(self, name, *values): 437 | "Push ``values`` onto the tail of the list ``name``" 438 | return self.execute_command('RPUSH', name, *values) 439 | 440 | # SPECIAL COMMANDS SUPPORTED BY LEDISDB 441 | def lclear(self, name): 442 | "Delete the key of ``name``" 443 | return self.execute_command("LCLEAR", name) 444 | 445 | def lmclear(self, *names): 446 | "Delete multiple keys of ``name``" 447 | return self.execute_command('LMCLEAR', *names) 448 | 449 | def lexpire(self, name, time): 450 | """ 451 | Set an expire flag on key ``name`` for ``time`` seconds. ``time`` 452 | can be represented by an integer or a Python timedelta object. 453 | """ 454 | if isinstance(time, datetime.timedelta): 455 | time = time.seconds + time.days * 24 * 3600 456 | return self.execute_command("LEXPIRE", name, time) 457 | 458 | def lexpireat(self, name, when): 459 | """ 460 | Set an expire flag on key ``name``. ``when`` can be represented as an integer 461 | indicating unix time or a Python datetime object. 462 | """ 463 | if isinstance(when, datetime.datetime): 464 | when = int(mod_time.mktime(when.timetuple())) 465 | return self.execute_command('LEXPIREAT', name, when) 466 | 467 | def lttl(self, name): 468 | "Returns the number of seconds until the key ``name`` will expire" 469 | return self.execute_command('LTTL', name) 470 | 471 | def lpersist(self, name): 472 | "Removes an expiration on ``name``" 473 | return self.execute_command('LPERSIST', name) 474 | 475 | def lxscan(self, key="", match=None, count=10): 476 | return self.scan_generic("LXSCAN", key=key, match=match, count=count) 477 | 478 | 479 | #### SET COMMANDS #### 480 | def sadd(self, name, *values): 481 | "Add ``value(s)`` to set ``name``" 482 | return self.execute_command('SADD', name, *values) 483 | 484 | def scard(self, name): 485 | "Return the number of elements in set ``name``" 486 | return self.execute_command('SCARD', name) 487 | 488 | def sdiff(self, keys, *args): 489 | "Return the difference of sets specified by ``keys``" 490 | args = list_or_args(keys, args) 491 | return self.execute_command('SDIFF', *args) 492 | 493 | def sdiffstore(self, dest, keys, *args): 494 | """ 495 | Store the difference of sets specified by ``keys`` into a new 496 | set named ``dest``. Returns the number of keys in the new set. 497 | """ 498 | args = list_or_args(keys, args) 499 | return self.execute_command('SDIFFSTORE', dest, *args) 500 | 501 | def sinter(self, keys, *args): 502 | "Return the intersection of sets specified by ``keys``" 503 | args = list_or_args(keys, args) 504 | return self.execute_command('SINTER', *args) 505 | 506 | def sinterstore(self, dest, keys, *args): 507 | """ 508 | Store the intersection of sets specified by ``keys`` into a new 509 | set named ``dest``. Returns the number of keys in the new set. 510 | """ 511 | args = list_or_args(keys, args) 512 | return self.execute_command('SINTERSTORE', dest, *args) 513 | 514 | def sismember(self, name, value): 515 | "Return a boolean indicating if ``value`` is a member of set ``name``" 516 | return self.execute_command('SISMEMBER', name, value) 517 | 518 | def smembers(self, name): 519 | "Return all members of the set ``name``" 520 | return self.execute_command('SMEMBERS', name) 521 | 522 | def srem(self, name, *values): 523 | "Remove ``values`` from set ``name``" 524 | return self.execute_command('SREM', name, *values) 525 | 526 | def sunion(self, keys, *args): 527 | "Return the union of sets specified by ``keys``" 528 | args = list_or_args(keys, args) 529 | return self.execute_command('SUNION', *args) 530 | 531 | def sunionstore(self, dest, keys, *args): 532 | """ 533 | Store the union of sets specified by ``keys`` into a new 534 | set named ``dest``. Returns the number of keys in the new set. 535 | """ 536 | args = list_or_args(keys, args) 537 | return self.execute_command('SUNIONSTORE', dest, *args) 538 | 539 | # SPECIAL COMMANDS SUPPORTED BY LEDISDB 540 | def sclear(self, name): 541 | "Delete key ``name`` from set" 542 | return self.execute_command('SCLEAR', name) 543 | 544 | def smclear(self, *names): 545 | "Delete multiple keys ``names`` from set" 546 | return self.execute_command('SMCLEAR', *names) 547 | 548 | def sexpire(self, name, time): 549 | """ 550 | Set an expire flag on key name for time milliseconds. 551 | time can be represented by an integer or a Python timedelta object. 552 | """ 553 | if isinstance(time, datetime.timedelta): 554 | time = time.seconds + time.days * 24 * 3600 555 | return self.execute_command('SEXPIRE', name, time) 556 | 557 | def sexpireat(self, name, when): 558 | """ 559 | Set an expire flag on key name. when can be represented as an integer 560 | representing unix time in milliseconds (unix time * 1000) or a 561 | Python datetime object. 562 | """ 563 | if isinstance(when, datetime.datetime): 564 | when = int(mod_time.mktime(when.timetuple())) 565 | return self.execute_command('SEXPIREAT', name, when) 566 | 567 | def sttl(self, name): 568 | "Returns the number of seconds until the key name will expire" 569 | return self.execute_command('STTL', name) 570 | 571 | def spersist(self, name): 572 | "Removes an expiration on name" 573 | return self.execute_command('SPERSIST', name) 574 | 575 | def sxscan(self, key="", match=None, count = 10): 576 | return self.scan_generic("SXSCAN", key=key, match=match, count=count) 577 | 578 | 579 | #### SORTED SET COMMANDS #### 580 | def zadd(self, name, *args, **kwargs): 581 | """ 582 | Set any number of score, element-name pairs to the key ``name``. Pairs 583 | can be specified in two ways: 584 | 585 | As *args, in the form of: score1, name1, score2, name2, ... 586 | or as **kwargs, in the form of: name1=score1, name2=score2, ... 587 | 588 | The following example would add four values to the 'my-key' key: 589 | ledis.zadd('my-key', 1.1, 'name1', 2.2, 'name2', name3=3.3, name4=4.4) 590 | """ 591 | pieces = [] 592 | if args: 593 | if len(args) % 2 != 0: 594 | raise LedisError("ZADD requires an equal number of " 595 | "values and scores") 596 | pieces.extend(args) 597 | for pair in iteritems(kwargs): 598 | pieces.append(pair[1]) 599 | pieces.append(pair[0]) 600 | return self.execute_command('ZADD', name, *pieces) 601 | 602 | def zcard(self, name): 603 | "Return the number of elements in the sorted set ``name``" 604 | return self.execute_command('ZCARD', name) 605 | 606 | def zcount(self, name, min, max): 607 | """ 608 | Return the number of elements in the sorted set at key ``name`` with a score 609 | between ``min`` and ``max``. 610 | The min and max arguments have the same semantic as described for ZRANGEBYSCORE. 611 | """ 612 | return self.execute_command('ZCOUNT', name, min, max) 613 | 614 | def zincrby(self, name, value, amount=1): 615 | "Increment the score of ``value`` in sorted set ``name`` by ``amount``" 616 | return self.execute_command('ZINCRBY', name, amount, value) 617 | 618 | def zrange(self, name, start, end, desc=False, withscores=False): 619 | """ 620 | Return a range of values from sorted set ``name`` between 621 | ``start`` and ``end`` sorted in ascending order. 622 | 623 | ``start`` and ``end`` can be negative, indicating the end of the range. 624 | 625 | ``desc`` a boolean indicating whether to sort the results descendingly 626 | 627 | ``withscores`` indicates to return the scores along with the values. 628 | The return type is a list of (value, score) pairs 629 | """ 630 | if desc: 631 | return self.zrevrange(name, start, end, withscores) 632 | 633 | pieces = ['ZRANGE', name, start, end] 634 | if withscores: 635 | pieces.append('withscores') 636 | options = { 637 | 'withscores': withscores} 638 | return self.execute_command(*pieces, **options) 639 | 640 | def zrangebyscore(self, name, min, max, start=None, num=None, 641 | withscores=False): 642 | """ 643 | Return a range of values from the sorted set ``name`` with scores 644 | between ``min`` and ``max``. 645 | 646 | If ``start`` and ``num`` are specified, then return a slice 647 | of the range. 648 | 649 | ``withscores`` indicates to return the scores along with the values. 650 | The return type is a list of (value, score) pairs 651 | """ 652 | if (start is not None and num is None) or \ 653 | (num is not None and start is None): 654 | raise LedisError("``start`` and ``num`` must both be specified") 655 | pieces = ['ZRANGEBYSCORE', name, min, max] 656 | if start is not None and num is not None: 657 | pieces.extend(['LIMIT', start, num]) 658 | if withscores: 659 | pieces.append('withscores') 660 | options = { 661 | 'withscores': withscores} 662 | return self.execute_command(*pieces, **options) 663 | 664 | def zrank(self, name, value): 665 | """ 666 | Returns a 0-based value indicating the rank of ``value`` in sorted set 667 | ``name`` 668 | """ 669 | return self.execute_command('ZRANK', name, value) 670 | 671 | def zrem(self, name, *values): 672 | "Remove member ``values`` from sorted set ``name``" 673 | return self.execute_command('ZREM', name, *values) 674 | 675 | def zremrangebyrank(self, name, min, max): 676 | """ 677 | Remove all elements in the sorted set ``name`` with ranks between 678 | ``min`` and ``max``. Values are 0-based, ordered from smallest score 679 | to largest. Values can be negative indicating the highest scores. 680 | Returns the number of elements removed 681 | """ 682 | return self.execute_command('ZREMRANGEBYRANK', name, min, max) 683 | 684 | def zremrangebyscore(self, name, min, max): 685 | """ 686 | Remove all elements in the sorted set ``name`` with scores 687 | between ``min`` and ``max``. Returns the number of elements removed. 688 | """ 689 | return self.execute_command('ZREMRANGEBYSCORE', name, min, max) 690 | 691 | def zrevrange(self, name, start, num, withscores=False): 692 | """ 693 | Return a range of values from sorted set ``name`` between 694 | ``start`` and ``num`` sorted in descending order. 695 | 696 | ``start`` and ``num`` can be negative, indicating the end of the range. 697 | 698 | ``withscores`` indicates to return the scores along with the values 699 | The return type is a list of (value, score) pairs 700 | """ 701 | pieces = ['ZREVRANGE', name, start, num] 702 | if withscores: 703 | pieces.append('withscores') 704 | options = {'withscores': withscores} 705 | return self.execute_command(*pieces, **options) 706 | 707 | def zrevrangebyscore(self, name, min, max, start=None, num=None, 708 | withscores=False): 709 | """ 710 | Return a range of values from the sorted set ``name`` with scores 711 | between ``min`` and ``max`` in descending order. 712 | 713 | If ``start`` and ``num`` are specified, then return a slice 714 | of the range. 715 | 716 | ``withscores`` indicates to return the scores along with the values. 717 | The return type is a list of (value, score) pairs 718 | """ 719 | if (start is not None and num is None) or \ 720 | (num is not None and start is None): 721 | raise LedisError("``start`` and ``num`` must both be specified") 722 | pieces = ['ZREVRANGEBYSCORE', name, min, max] 723 | if start is not None and num is not None: 724 | pieces.extend(['LIMIT', start, num]) 725 | if withscores: 726 | pieces.append('withscores') 727 | options = {'withscores': withscores} 728 | return self.execute_command(*pieces, **options) 729 | 730 | def zrevrank(self, name, value): 731 | """ 732 | Returns a 0-based value indicating the descending rank of 733 | ``value`` in sorted set ``name`` 734 | """ 735 | return self.execute_command('ZREVRANK', name, value) 736 | 737 | def zscore(self, name, value): 738 | "Return the score of element ``value`` in sorted set ``name``" 739 | return self.execute_command('ZSCORE', name, value) 740 | 741 | # SPECIAL COMMANDS SUPPORTED BY LEDISDB 742 | def zclear(self, name): 743 | "Delete key of ``name`` from sorted set" 744 | return self.execute_command('ZCLEAR', name) 745 | 746 | def zmclear(self, *names): 747 | "Delete multiple keys of ``names`` from sorted set" 748 | return self.execute_command('ZMCLEAR', *names) 749 | 750 | def zexpire(self, name, time): 751 | "Set timeout on key ``name`` with ``time``" 752 | if isinstance(time, datetime.timedelta): 753 | time = time.seconds + time.days * 24 * 3600 754 | return self.execute_command('ZEXPIRE', name, time) 755 | 756 | def zexpireat(self, name, when): 757 | """ 758 | Set an expire flag on key name for time seconds. time can be represented by 759 | an integer or a Python timedelta object. 760 | """ 761 | 762 | if isinstance(when, datetime.datetime): 763 | when = int(mod_time.mktime(when.timetuple())) 764 | return self.execute_command('ZEXPIREAT', name, when) 765 | 766 | def zttl(self, name): 767 | "Returns the number of seconds until the key name will expire" 768 | return self.execute_command('ZTTL', name) 769 | 770 | def zpersist(self, name): 771 | "Removes an expiration on name" 772 | return self.execute_command('ZPERSIST', name) 773 | 774 | 775 | def scan_generic(self, scan_type, key="", match=None, count=10): 776 | pieces = [key] 777 | if match is not None: 778 | pieces.extend([Token("MATCH"), match]) 779 | pieces.extend([Token("count"), count]) 780 | scan_type = scan_type.upper() 781 | return self.execute_command(scan_type, *pieces) 782 | 783 | def zxscan(self, key="", match=None, count=10): 784 | return self.scan_generic("ZXSCAN", key=key, match=match, count=count) 785 | 786 | #### HASH COMMANDS #### 787 | def hdel(self, name, *keys): 788 | "Delete ``keys`` from hash ``name``" 789 | return self.execute_command('HDEL', name, *keys) 790 | 791 | def hexists(self, name, key): 792 | "Returns a boolean indicating if ``key`` exists within hash ``name``" 793 | return self.execute_command('HEXISTS', name, key) 794 | 795 | def hget(self, name, key): 796 | "Return the value of ``key`` within the hash ``name``" 797 | return self.execute_command('HGET', name, key) 798 | 799 | def hgetall(self, name): 800 | "Return a Python dict of the hash's name/value pairs" 801 | return self.execute_command('HGETALL', name) 802 | 803 | def hincrby(self, name, key, amount=1): 804 | "Increment the value of ``key`` in hash ``name`` by ``amount``" 805 | return self.execute_command('HINCRBY', name, key, amount) 806 | 807 | def hkeys(self, name): 808 | "Return the list of keys within hash ``name``" 809 | return self.execute_command('HKEYS', name) 810 | 811 | def hlen(self, name): 812 | "Return the number of elements in hash ``name``" 813 | return self.execute_command('HLEN', name) 814 | 815 | def hmget(self, name, keys, *args): 816 | "Returns a list of values ordered identically to ``keys``" 817 | args = list_or_args(keys, args) 818 | return self.execute_command('HMGET', name, *args) 819 | 820 | def hmset(self, name, mapping): 821 | """ 822 | Sets each key in the ``mapping`` dict to its corresponding value 823 | in the hash ``name`` 824 | """ 825 | if not mapping: 826 | raise DataError("'hmset' with 'mapping' of length 0") 827 | items = [] 828 | for pair in iteritems(mapping): 829 | items.extend(pair) 830 | return self.execute_command('HMSET', name, *items) 831 | 832 | def hset(self, name, key, value): 833 | """ 834 | Set ``key`` to ``value`` within hash ``name`` 835 | Returns 1 if HSET created a new field, otherwise 0 836 | """ 837 | return self.execute_command('HSET', name, key, value) 838 | 839 | def hvals(self, name): 840 | "Return the list of values within hash ``name``" 841 | return self.execute_command('HVALS', name) 842 | 843 | # SPECIAL COMMANDS SUPPORTED BY LEDISDB 844 | def hclear(self, name): 845 | "Delete key ``name`` from hash" 846 | return self.execute_command('HCLEAR', name) 847 | 848 | def hmclear(self, *names): 849 | "Delete multiple keys ``names`` from hash" 850 | return self.execute_command('HMCLEAR', *names) 851 | 852 | def hexpire(self, name, time): 853 | """ 854 | Set an expire flag on key name for time milliseconds. 855 | time can be represented by an integer or a Python timedelta object. 856 | """ 857 | if isinstance(time, datetime.timedelta): 858 | time = time.seconds + time.days * 24 * 3600 859 | return self.execute_command('HEXPIRE', name, time) 860 | 861 | def hexpireat(self, name, when): 862 | """ 863 | Set an expire flag on key name. when can be represented as an integer representing 864 | unix time in milliseconds (unix time * 1000) or a Python datetime object. 865 | """ 866 | if isinstance(when, datetime.datetime): 867 | when = int(mod_time.mktime(when.timetuple())) 868 | return self.execute_command('HEXPIREAT', name, when) 869 | 870 | def httl(self, name): 871 | "Returns the number of seconds until the key name will expire" 872 | return self.execute_command('HTTL', name) 873 | 874 | def hpersist(self, name): 875 | "Removes an expiration on name" 876 | return self.execute_command('HPERSIST', name) 877 | 878 | def hxscan(self, key="", match=None, count=10): 879 | return self.scan_generic("HXSCAN", key=key, match=match, count=count) 880 | 881 | 882 | ### BIT COMMANDS 883 | def bget(self, name): 884 | "" 885 | return self.execute_command("BGET", name) 886 | 887 | def bdelete(self, name): 888 | "" 889 | return self.execute_command("BDELETE", name) 890 | 891 | def bsetbit(self, name, offset, value): 892 | "" 893 | value = value and 1 or 0 894 | return self.execute_command("BSETBIT", name, offset, value) 895 | 896 | def bgetbit(self, name, offset): 897 | "" 898 | return self.execute_command("BGETBIT", name, offset) 899 | 900 | def bmsetbit(self, name, *args): 901 | """ 902 | Set any number of offset, value pairs to the key ``name``. Pairs can be 903 | specified in the following way: 904 | 905 | offset1, value1, offset2, value2, ... 906 | """ 907 | pieces = [] 908 | if args: 909 | if len(args) % 2 != 0: 910 | raise LedisError("BMSETBIT requires an equal number of " 911 | "offset and value") 912 | pieces.extend(args) 913 | return self.execute_command("BMSETBIT", name, *pieces) 914 | 915 | def bcount(self, key, start=None, end=None): 916 | "" 917 | params = [key] 918 | if start is not None and end is not None: 919 | params.append(start) 920 | params.append(end) 921 | elif (start is not None and end is None) or \ 922 | (start is None and end is not None): 923 | raise LedisError("Both start and end must be specified") 924 | return self.execute_command("BCOUNT", *params) 925 | 926 | def bopt(self, operation, dest, *keys): 927 | """ 928 | Perform a bitwise operation using ``operation`` between ``keys`` and 929 | store the result in ``dest``. 930 | ``operation`` is one of `and`, `or`, `xor`, `not`. 931 | """ 932 | return self.execute_command('BOPT', operation, dest, *keys) 933 | 934 | def bexpire(self, name, time): 935 | "Set timeout on key ``name`` with ``time``" 936 | if isinstance(time, datetime.timedelta): 937 | time = time.seconds + time.days * 24 * 3600 938 | return self.execute_command('BEXPIRE', name, time) 939 | 940 | def bexpireat(self, name, when): 941 | """ 942 | Set an expire flag on key name for time seconds. time can be represented by 943 | an integer or a Python timedelta object. 944 | """ 945 | if isinstance(when, datetime.datetime): 946 | when = int(mod_time.mktime(when.timetuple())) 947 | return self.execute_command('BEXPIREAT', name, when) 948 | 949 | def bttl(self, name): 950 | "Returns the number of seconds until the key name will expire" 951 | return self.execute_command('BTTL', name) 952 | 953 | def bpersist(self, name): 954 | "Removes an expiration on name" 955 | return self.execute_command('BPERSIST', name) 956 | 957 | def bxscan(self, key="", match=None, count=10): 958 | return self.scan_generic("BXSCAN", key=key, match=match, count=count) 959 | 960 | def eval(self, script, keys, *args): 961 | n = len(keys) 962 | args = list_or_args(keys, args) 963 | return self.execute_command('EVAL', script, n, *args) 964 | 965 | def evalsha(self, sha1, keys, *args): 966 | n = len(keys) 967 | args = list_or_args(keys, args) 968 | return self.execute_command('EVALSHA', sha1, n, *args) 969 | 970 | def scriptload(self, script): 971 | return self.execute_command('SCRIPT', 'LOAD', script) 972 | 973 | def scriptexists(self, *args): 974 | return self.execute_command('SCRIPT', 'EXISTS', *args) 975 | 976 | def scriptflush(self): 977 | return self.execute_command('SCRIPT', 'FLUSH') 978 | 979 | 980 | class Transaction(Ledis): 981 | def __init__(self, connection_pool, response_callbacks): 982 | self.connection_pool = connection_pool 983 | self.response_callbacks = response_callbacks 984 | self.connection = None 985 | 986 | def execute_command(self, *args, **options): 987 | "Execute a command and return a parsed response" 988 | command_name = args[0] 989 | 990 | connection = self.connection 991 | if self.connection is None: 992 | raise TxNotBeginError 993 | 994 | try: 995 | connection.send_command(*args) 996 | return self.parse_response(connection, command_name, **options) 997 | except ConnectionError: 998 | connection.disconnect() 999 | connection.send_command(*args) 1000 | return self.parse_response(connection, command_name, **options) 1001 | 1002 | def begin(self): 1003 | self.connection = self.connection_pool.get_connection('begin') 1004 | return self.execute_command("BEGIN") 1005 | 1006 | def commit(self): 1007 | res = self.execute_command("COMMIT") 1008 | self.connection_pool.release(self.connection) 1009 | self.connection = None 1010 | return res 1011 | 1012 | def rollback(self): 1013 | res = self.execute_command("ROLLBACK") 1014 | self.connection_pool.release(self.connection) 1015 | self.connection = None 1016 | return res 1017 | 1018 | -------------------------------------------------------------------------------- /ledis/connection.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | import os 3 | import socket 4 | import sys 5 | 6 | from ledis._compat import (b, xrange, imap, byte_to_chr, unicode, bytes, long, 7 | BytesIO, nativestr, basestring, iteritems, 8 | LifoQueue, Empty, Full, urlparse, parse_qs) 9 | from ledis.exceptions import ( 10 | LedisError, 11 | ConnectionError, 12 | BusyLoadingError, 13 | ResponseError, 14 | InvalidResponse, 15 | ExecAbortError, 16 | ) 17 | 18 | 19 | SYM_STAR = b('*') 20 | SYM_DOLLAR = b('$') 21 | SYM_CRLF = b('\r\n') 22 | SYM_LF = b('\n') 23 | 24 | 25 | class PythonParser(object): 26 | "Plain Python parsing class" 27 | MAX_READ_LENGTH = 1000000 28 | encoding = None 29 | 30 | EXCEPTION_CLASSES = { 31 | 'ERR': ResponseError, 32 | 'EXECABORT': ExecAbortError, 33 | 'LOADING': BusyLoadingError, 34 | } 35 | 36 | def __init__(self): 37 | self._fp = None 38 | 39 | def __del__(self): 40 | try: 41 | self.on_disconnect() 42 | except Exception: 43 | pass 44 | 45 | def on_connect(self, connection): 46 | "Called when the socket connects" 47 | self._fp = connection._sock.makefile('rb') 48 | if connection.decode_responses: 49 | self.encoding = connection.encoding 50 | 51 | def on_disconnect(self): 52 | "Called when the socket disconnects" 53 | if self._fp is not None: 54 | self._fp.close() 55 | self._fp = None 56 | 57 | def read(self, length=None): 58 | """ 59 | Read a line from the socket if no length is specified, 60 | otherwise read ``length`` bytes. Always strip away the newlines. 61 | """ 62 | try: 63 | if length is not None: 64 | bytes_left = length + 2 # read the line ending 65 | if length > self.MAX_READ_LENGTH: 66 | # apparently reading more than 1MB or so from a windows 67 | # socket can cause MemoryErrors. See: 68 | # https://github.com/andymccurdy/redis-py/issues/205 69 | # read smaller chunks at a time to work around this 70 | try: 71 | buf = BytesIO() 72 | while bytes_left > 0: 73 | read_len = min(bytes_left, self.MAX_READ_LENGTH) 74 | buf.write(self._fp.read(read_len)) 75 | bytes_left -= read_len 76 | buf.seek(0) 77 | return buf.read(length) 78 | finally: 79 | buf.close() 80 | return self._fp.read(bytes_left)[:-2] 81 | 82 | # no length, read a full line 83 | return self._fp.readline()[:-2] 84 | except (socket.error, socket.timeout): 85 | e = sys.exc_info()[1] 86 | raise ConnectionError("Error while reading from socket: %s" % 87 | (e.args,)) 88 | 89 | def parse_error(self, response): 90 | "Parse an error response" 91 | error_code = response.split(' ')[0] 92 | if error_code in self.EXCEPTION_CLASSES: 93 | response = response[len(error_code) + 1:] 94 | return self.EXCEPTION_CLASSES[error_code](response) 95 | return ResponseError(response) 96 | 97 | def read_response(self): 98 | response = self.read() 99 | if not response: 100 | raise ConnectionError("Socket closed on remote end") 101 | 102 | byte, response = byte_to_chr(response[0]), response[1:] 103 | 104 | if byte not in ('-', '+', ':', '$', '*'): 105 | raise InvalidResponse("Protocol Error") 106 | 107 | # server returned an error 108 | if byte == '-': 109 | response = nativestr(response) 110 | error = self.parse_error(response) 111 | # if the error is a ConnectionError, raise immediately so the user 112 | # is notified 113 | if isinstance(error, ConnectionError): 114 | raise error 115 | # otherwise, we're dealing with a ResponseError that might belong 116 | # inside a pipeline response. the connection's read_response() 117 | # and/or the pipeline's execute() will raise this error if 118 | # necessary, so just return the exception instance here. 119 | return error 120 | # single value 121 | elif byte == '+': 122 | pass 123 | # int value 124 | elif byte == ':': 125 | response = long(response) 126 | # bulk response 127 | elif byte == '$': 128 | length = int(response) 129 | if length == -1: 130 | return None 131 | response = self.read(length) 132 | # multi-bulk response 133 | elif byte == '*': 134 | length = int(response) 135 | if length == -1: 136 | return None 137 | response = [self.read_response() for i in xrange(length)] 138 | if isinstance(response, bytes) and self.encoding: 139 | response = response.decode(self.encoding) 140 | return response 141 | 142 | 143 | DefaultParser = PythonParser 144 | 145 | 146 | class Connection(object): 147 | "Manages TCP communication to and from a Ledis server" 148 | def __init__(self, host='localhost', port=6380, db=0, 149 | socket_timeout=None, encoding='utf-8', 150 | encoding_errors='strict', decode_responses=False, 151 | parser_class=DefaultParser): 152 | self.pid = os.getpid() 153 | self.host = host 154 | self.port = port 155 | self.db = db 156 | self.socket_timeout = socket_timeout 157 | self.encoding = encoding 158 | self.encoding_errors = encoding_errors 159 | self.decode_responses = decode_responses 160 | self._sock = None 161 | self._parser = parser_class() 162 | 163 | def __del__(self): 164 | try: 165 | self.disconnect() 166 | except Exception: 167 | pass 168 | 169 | def connect(self): 170 | "Connects to the Ledis server if not already connected" 171 | if self._sock: 172 | return 173 | try: 174 | sock = self._connect() 175 | except socket.error: 176 | e = sys.exc_info()[1] 177 | raise ConnectionError(self._error_message(e)) 178 | 179 | self._sock = sock 180 | self.on_connect() 181 | 182 | def _connect(self): 183 | "Create a TCP socket connection" 184 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 185 | sock.settimeout(self.socket_timeout) 186 | sock.connect((self.host, self.port)) 187 | return sock 188 | 189 | def _error_message(self, exception): 190 | # args for socket.error can either be (errno, "message") 191 | # or just "message" 192 | if len(exception.args) == 1: 193 | return "Error connecting to %s:%s. %s." % \ 194 | (self.host, self.port, exception.args[0]) 195 | else: 196 | return "Error %s connecting %s:%s. %s." % \ 197 | (exception.args[0], self.host, self.port, exception.args[1]) 198 | 199 | def on_connect(self): 200 | "Initialize the connection, authenticate and select a database" 201 | self._parser.on_connect(self) 202 | 203 | # if a database is specified, switch to it 204 | if self.db: 205 | self.send_command('SELECT', self.db) 206 | if nativestr(self.read_response()) != 'OK': 207 | raise ConnectionError('Invalid Database') 208 | 209 | def disconnect(self): 210 | "Disconnects from the Ledis server" 211 | self._parser.on_disconnect() 212 | if self._sock is None: 213 | return 214 | try: 215 | self._sock.close() 216 | except socket.error: 217 | pass 218 | self._sock = None 219 | 220 | def send_packed_command(self, command): 221 | "Send an already packed command to the Ledis server" 222 | if not self._sock: 223 | self.connect() 224 | try: 225 | self._sock.sendall(command) 226 | except socket.error: 227 | e = sys.exc_info()[1] 228 | self.disconnect() 229 | if len(e.args) == 1: 230 | _errno, errmsg = 'UNKNOWN', e.args[0] 231 | else: 232 | _errno, errmsg = e.args 233 | raise ConnectionError("Error %s while writing to socket. %s." % 234 | (_errno, errmsg)) 235 | except Exception: 236 | self.disconnect() 237 | raise 238 | 239 | def send_command(self, *args): 240 | "Pack and send a command to the Ledis server" 241 | self.send_packed_command(self.pack_command(*args)) 242 | 243 | def read_response(self): 244 | "Read the response from a previously sent command" 245 | try: 246 | response = self._parser.read_response() 247 | except Exception: 248 | self.disconnect() 249 | raise 250 | if isinstance(response, ResponseError): 251 | raise response 252 | return response 253 | 254 | def encode(self, value): 255 | "Return a bytestring representation of the value" 256 | if isinstance(value, bytes): 257 | return value 258 | if isinstance(value, float): 259 | value = repr(value) 260 | if not isinstance(value, basestring): 261 | value = str(value) 262 | if isinstance(value, unicode): 263 | value = value.encode(self.encoding, self.encoding_errors) 264 | return value 265 | 266 | def pack_command(self, *args): 267 | "Pack a series of arguments into a value Ledis command" 268 | output = SYM_STAR + b(str(len(args))) + SYM_CRLF 269 | for enc_value in imap(self.encode, args): 270 | output += SYM_DOLLAR 271 | output += b(str(len(enc_value))) 272 | output += SYM_CRLF 273 | output += enc_value 274 | output += SYM_CRLF 275 | return output 276 | 277 | 278 | class UnixDomainSocketConnection(Connection): 279 | def __init__(self, path='', db=0, socket_timeout=None, encoding='utf-8', 280 | encoding_errors='strict', decode_responses=False, 281 | parser_class=DefaultParser): 282 | self.pid = os.getpid() 283 | self.path = path 284 | self.db = db 285 | self.socket_timeout = socket_timeout 286 | self.encoding = encoding 287 | self.encoding_errors = encoding_errors 288 | self.decode_responses = decode_responses 289 | self._sock = None 290 | self._parser = parser_class() 291 | 292 | def _connect(self): 293 | "Create a Unix domain socket connection" 294 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 295 | sock.settimeout(self.socket_timeout) 296 | sock.connect(self.path) 297 | return sock 298 | 299 | def _error_message(self, exception): 300 | # args for socket.error can either be (errno, "message") 301 | # or just "message" 302 | if len(exception.args) == 1: 303 | return "Error connecting to unix socket: %s. %s." % \ 304 | (self.path, exception.args[0]) 305 | else: 306 | return "Error %s connecting to unix socket: %s. %s." % \ 307 | (exception.args[0], self.path, exception.args[1]) 308 | 309 | 310 | # TODO: add ability to block waiting on a connection to be released 311 | class ConnectionPool(object): 312 | "Generic connection pool" 313 | @classmethod 314 | def from_url(cls, url, db=None, **kwargs): 315 | """ 316 | Return a connection pool configured from the given URL. 317 | 318 | For example:: 319 | 320 | ledis://localhost:6380/0 321 | unix:///path/to/socket.sock?db=0 322 | 323 | Three URL schemes are supported: 324 | ledis:// creates a normal TCP socket connection 325 | unix:// creates a Unix Domain Socket connection 326 | 327 | There are several ways to specify a database number. The parse function 328 | will return the first specified option: 329 | 1. A ``db`` querystring option, e.g. ledis://localhost?db=0 330 | 2. If using the ledis:// scheme, the path argument of the url, e.g. 331 | ledis://localhost/0 332 | 3. The ``db`` argument to this function. 333 | 334 | If none of these options are specified, db=0 is used. 335 | 336 | Any additional querystring arguments and keyword arguments will be 337 | passed along to the ConnectionPool class's initializer. In the case 338 | of conflicting arguments, querystring arguments always win. 339 | """ 340 | url_string = url 341 | url = urlparse(url) 342 | qs = '' 343 | 344 | # in python2.6, custom URL schemes don't recognize querystring values 345 | # they're left as part of the url.path. 346 | if '?' in url.path and not url.query: 347 | # chop the querystring including the ? off the end of the url 348 | # and reparse it. 349 | qs = url.path.split('?', 1)[1] 350 | url = urlparse(url_string[:-(len(qs) + 1)]) 351 | else: 352 | qs = url.query 353 | 354 | url_options = {} 355 | 356 | for name, value in iteritems(parse_qs(qs)): 357 | if value and len(value) > 0: 358 | url_options[name] = value[0] 359 | 360 | # We only support ledis:// and unix:// schemes. 361 | if url.scheme == 'unix': 362 | url_options.update({ 363 | 'path': url.path, 364 | 'connection_class': UnixDomainSocketConnection, 365 | }) 366 | 367 | else: 368 | url_options.update({ 369 | 'host': url.hostname, 370 | 'port': int(url.port or 6380), 371 | }) 372 | 373 | # If there's a path argument, use it as the db argument if a 374 | # querystring value wasn't specified 375 | if 'db' not in url_options and url.path: 376 | try: 377 | url_options['db'] = int(url.path.replace('/', '')) 378 | except (AttributeError, ValueError): 379 | pass 380 | 381 | # last shot at the db value 382 | url_options['db'] = int(url_options.get('db', db or 0)) 383 | 384 | # update the arguments from the URL values 385 | kwargs.update(url_options) 386 | return cls(**kwargs) 387 | 388 | def __init__(self, connection_class=Connection, max_connections=None, 389 | **connection_kwargs): 390 | self.pid = os.getpid() 391 | self.connection_class = connection_class 392 | self.connection_kwargs = connection_kwargs 393 | self.max_connections = max_connections or 2 ** 31 394 | self._created_connections = 0 395 | self._available_connections = [] 396 | self._in_use_connections = set() 397 | 398 | def _checkpid(self): 399 | if self.pid != os.getpid(): 400 | self.disconnect() 401 | self.__init__(self.connection_class, self.max_connections, 402 | **self.connection_kwargs) 403 | 404 | def get_connection(self, command_name, *keys, **options): 405 | "Get a connection from the pool" 406 | self._checkpid() 407 | try: 408 | connection = self._available_connections.pop() 409 | except IndexError: 410 | connection = self.make_connection() 411 | self._in_use_connections.add(connection) 412 | return connection 413 | 414 | def make_connection(self): 415 | "Create a new connection" 416 | if self._created_connections >= self.max_connections: 417 | raise ConnectionError("Too many connections") 418 | self._created_connections += 1 419 | return self.connection_class(**self.connection_kwargs) 420 | 421 | def release(self, connection): 422 | "Releases the connection back to the pool" 423 | self._checkpid() 424 | if connection.pid == self.pid: 425 | self._in_use_connections.remove(connection) 426 | self._available_connections.append(connection) 427 | 428 | def disconnect(self): 429 | "Disconnects all connections in the pool" 430 | all_conns = chain(self._available_connections, 431 | self._in_use_connections) 432 | for connection in all_conns: 433 | connection.disconnect() 434 | 435 | 436 | class BlockingConnectionPool(object): 437 | """ 438 | Thread-safe blocking connection pool:: 439 | 440 | >>> from ledis.client import Ledis 441 | >>> client = Ledis(connection_pool=BlockingConnectionPool()) 442 | 443 | It performs the same function as the default 444 | ``:py:class: ~ledis.connection.ConnectionPool`` implementation, in that, 445 | it maintains a pool of reusable connections that can be shared by 446 | multiple ledis clients (safely across threads if required). 447 | 448 | The difference is that, in the event that a client tries to get a 449 | connection from the pool when all of connections are in use, rather than 450 | raising a ``:py:class: ~ledis.exceptions.ConnectionError`` (as the default 451 | ``:py:class: ~ledis.connection.ConnectionPool`` implementation does), it 452 | makes the client wait ("blocks") for a specified number of seconds until 453 | a connection becomes available. 454 | 455 | Use ``max_connections`` to increase / decrease the pool size:: 456 | 457 | >>> pool = BlockingConnectionPool(max_connections=10) 458 | 459 | Use ``timeout`` to tell it either how many seconds to wait for a connection 460 | to become available, or to block forever: 461 | 462 | # Block forever. 463 | >>> pool = BlockingConnectionPool(timeout=None) 464 | 465 | # Raise a ``ConnectionError`` after five seconds if a connection is 466 | # not available. 467 | >>> pool = BlockingConnectionPool(timeout=5) 468 | """ 469 | def __init__(self, max_connections=50, timeout=20, connection_class=None, 470 | queue_class=None, **connection_kwargs): 471 | "Compose and assign values." 472 | # Compose. 473 | if connection_class is None: 474 | connection_class = Connection 475 | if queue_class is None: 476 | queue_class = LifoQueue 477 | 478 | # Assign. 479 | self.connection_class = connection_class 480 | self.connection_kwargs = connection_kwargs 481 | self.queue_class = queue_class 482 | self.max_connections = max_connections 483 | self.timeout = timeout 484 | 485 | # Validate the ``max_connections``. With the "fill up the queue" 486 | # algorithm we use, it must be a positive integer. 487 | is_valid = isinstance(max_connections, int) and max_connections > 0 488 | if not is_valid: 489 | raise ValueError('``max_connections`` must be a positive integer') 490 | 491 | # Get the current process id, so we can disconnect and reinstantiate if 492 | # it changes. 493 | self.pid = os.getpid() 494 | 495 | # Create and fill up a thread safe queue with ``None`` values. 496 | self.pool = self.queue_class(max_connections) 497 | while True: 498 | try: 499 | self.pool.put_nowait(None) 500 | except Full: 501 | break 502 | 503 | # Keep a list of actual connection instances so that we can 504 | # disconnect them later. 505 | self._connections = [] 506 | 507 | def _checkpid(self): 508 | """ 509 | Check the current process id. If it has changed, disconnect and 510 | re-instantiate this connection pool instance. 511 | """ 512 | # Get the current process id. 513 | pid = os.getpid() 514 | 515 | # If it hasn't changed since we were instantiated, then we're fine, so 516 | # just exit, remaining connected. 517 | if self.pid == pid: 518 | return 519 | 520 | # If it has changed, then disconnect and re-instantiate. 521 | self.disconnect() 522 | self.reinstantiate() 523 | 524 | def make_connection(self): 525 | "Make a fresh connection." 526 | connection = self.connection_class(**self.connection_kwargs) 527 | self._connections.append(connection) 528 | return connection 529 | 530 | def get_connection(self, command_name, *keys, **options): 531 | """ 532 | Get a connection, blocking for ``self.timeout`` until a connection 533 | is available from the pool. 534 | 535 | If the connection returned is ``None`` then creates a new connection. 536 | Because we use a last-in first-out queue, the existing connections 537 | (having been returned to the pool after the initial ``None`` values 538 | were added) will be returned before ``None`` values. This means we only 539 | create new connections when we need to, i.e.: the actual number of 540 | connections will only increase in response to demand. 541 | """ 542 | # Make sure we haven't changed process. 543 | self._checkpid() 544 | 545 | # Try and get a connection from the pool. If one isn't available within 546 | # self.timeout then raise a ``ConnectionError``. 547 | connection = None 548 | try: 549 | connection = self.pool.get(block=True, timeout=self.timeout) 550 | except Empty: 551 | # Note that this is not caught by the ledis client and will be 552 | # raised unless handled by application code. If you want never to 553 | raise ConnectionError("No connection available.") 554 | 555 | # If the ``connection`` is actually ``None`` then that's a cue to make 556 | # a new connection to add to the pool. 557 | if connection is None: 558 | connection = self.make_connection() 559 | 560 | return connection 561 | 562 | def release(self, connection): 563 | "Releases the connection back to the pool." 564 | # Make sure we haven't changed process. 565 | self._checkpid() 566 | 567 | # Put the connection back into the pool. 568 | try: 569 | self.pool.put_nowait(connection) 570 | except Full: 571 | # This shouldn't normally happen but might perhaps happen after a 572 | # reinstantiation. So, we can handle the exception by not putting 573 | # the connection back on the pool, because we definitely do not 574 | # want to reuse it. 575 | pass 576 | 577 | def disconnect(self): 578 | "Disconnects all connections in the pool." 579 | for connection in self._connections: 580 | connection.disconnect() 581 | 582 | def reinstantiate(self): 583 | """ 584 | Reinstatiate this instance within a new process with a new connection 585 | pool set. 586 | """ 587 | self.__init__(max_connections=self.max_connections, 588 | timeout=self.timeout, 589 | connection_class=self.connection_class, 590 | queue_class=self.queue_class, **self.connection_kwargs) 591 | 592 | 593 | class Token(object): 594 | """ 595 | Literal strings in Redis commands, such as the command names and any 596 | hard-coded arguments are wrapped in this class so we know not to apply 597 | and encoding rules on them. 598 | """ 599 | def __init__(self, value): 600 | if isinstance(value, Token): 601 | value = value.value 602 | self.value = value 603 | 604 | def __repr__(self): 605 | return self.value 606 | 607 | def __str__(self): 608 | return self.value 609 | 610 | 611 | -------------------------------------------------------------------------------- /ledis/exceptions.py: -------------------------------------------------------------------------------- 1 | "Core exceptions raised by the LedisDB client" 2 | 3 | 4 | class LedisError(Exception): 5 | pass 6 | 7 | class ServerError(LedisError): 8 | pass 9 | 10 | 11 | class ConnectionError(ServerError): 12 | pass 13 | 14 | 15 | class BusyLoadingError(ConnectionError): 16 | pass 17 | 18 | 19 | class TimeoutError(LedisError): 20 | pass 21 | 22 | 23 | class InvalidResponse(ServerError): 24 | pass 25 | 26 | 27 | class ResponseError(LedisError): 28 | pass 29 | 30 | 31 | class DataError(LedisError): 32 | pass 33 | 34 | 35 | class ExecAbortError(ResponseError): 36 | pass 37 | 38 | class TxNotBeginError(LedisError): 39 | pass -------------------------------------------------------------------------------- /ledis/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def from_url(url, db=None, **kwargs): 3 | """ 4 | Returns an active Ledis client generated from the given database URL. 5 | 6 | Will attempt to extract the database id from the path url fragment, if 7 | none is provided. 8 | """ 9 | from ledis.client import Ledis 10 | return Ledis.from_url(url, db, **kwargs) 11 | -------------------------------------------------------------------------------- /redis-py_license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andy McCurdy 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from ledis import __version__ 6 | 7 | try: 8 | from setuptools import setup 9 | from setuptools.command.test import test as TestCommand 10 | 11 | class PyTest(TestCommand): 12 | def finalize_options(self): 13 | TestCommand.finalize_options(self) 14 | self.test_args = [] 15 | self.test_suite = True 16 | 17 | def run_tests(self): 18 | # import here, because outside the eggs aren't loaded 19 | import pytest 20 | errno = pytest.main(self.test_args) 21 | sys.exit(errno) 22 | 23 | except ImportError: 24 | 25 | from distutils.core import setup 26 | PyTest = lambda x: x 27 | 28 | 29 | setup( 30 | name='ledis', 31 | version=__version__, 32 | description='Python client for ledisdb key-value database', 33 | long_description='Python client for ledisdb key-value database', 34 | url='https://github.com/siddontang/ledisdb', 35 | keywords=['ledis', 'key-value store'], 36 | license='MIT', 37 | packages=['ledis'], 38 | tests_require=['pytest>=2.5.0'], 39 | cmdclass={'test': PyTest}, 40 | classifiers=[ 41 | 'Development Status :: 5 - Production/Stable', 42 | 'Environment :: Console', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 2.6', 48 | 'Programming Language :: Python :: 2.7', 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holys/ledis-py/20312ca3c43e3b345db799aff0e2d7180f5704b4/tests/__init__.py -------------------------------------------------------------------------------- /tests/all.sh: -------------------------------------------------------------------------------- 1 | dbs=(leveldb rocksdb hyperleveldb goleveldb boltdb lmdb) 2 | for db in "${dbs[@]}" 3 | do 4 | killall ledis-server 5 | ledis-server -db_name=$db & 6 | py.test 7 | done 8 | -------------------------------------------------------------------------------- /tests/test_cmd_bit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for bit commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b 10 | from util import expire_at, expire_at_seconds 11 | 12 | l = ledis.Ledis(port=6380) 13 | 14 | 15 | class TestCmdBit(unittest.TestCase): 16 | def setUp(self): 17 | pass 18 | 19 | def tearDown(self): 20 | l.flushdb() 21 | 22 | def test_bget(self): 23 | "bget is the same as get in K/V commands" 24 | l.bmsetbit('a', 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 0) 25 | assert l.bget('a') == b('\x7f') 26 | 27 | def test_bdelete(self): 28 | l.bsetbit('a', 0, 1) 29 | assert l.bdelete('a') 30 | assert not l.bdelete('non_exists_key') 31 | 32 | def test_get_set_bit(self): 33 | assert not l.bgetbit('a', 5) 34 | assert not l.bsetbit('a', 5, True) 35 | assert l.bgetbit('a', 5) 36 | 37 | assert not l.bsetbit('a', 4, False) 38 | assert not l.bgetbit('a', 4) 39 | 40 | assert not l.bsetbit('a', 4, True) 41 | assert l.bgetbit('a', 4) 42 | 43 | assert l.bsetbit('a', 5, True) 44 | assert l.bgetbit('a', 5) 45 | 46 | def test_bmsetbit(self): 47 | assert l.bmsetbit('a', 0, 1, 2, 1, 3, 1) == 3 48 | 49 | def test_bcount(self): 50 | l.bsetbit('a', 5, 1) 51 | assert l.bcount('a') == 1 52 | l.bsetbit('a', 6, 1) 53 | assert l.bcount('a') == 2 54 | l.bsetbit('a', 5, 0) 55 | assert l.bcount('a') == 1 56 | l.bmsetbit('a', 10, 1, 20, 1, 30, 1, 40, 1) 57 | assert l.bcount('a') == 5 58 | assert l.bcount('a', 0, 10) == 2 59 | assert l.bcount('a', 20, 30) == 2 60 | assert l.bcount('a', 10, 10) == 1 61 | 62 | def test_bopt_not_empty_string(self): 63 | l.bopt('not', 'r', 'a') 64 | assert l.bget('r') is None 65 | 66 | def test_bopt(self): 67 | l.bmsetbit('a1', 10, 1, 30, 1, 50, 1, 70, 1, 90, 1) 68 | l.bmsetbit('a2', 20, 1, 40, 1, 60, 1, 80, 1, 100, 1) 69 | assert l.bopt('and', 'res1', 'a1', 'a2') == 101 70 | assert l.bcount('res1') == 0 71 | 72 | assert l.bopt('or', 'res2', 'a1', 'a2') == 101 73 | assert l.bcount('res2') == 10 74 | 75 | assert l.bopt('xor', 'res3', 'a1', 'a2') == 101 76 | assert l.bcount('res3') == 10 77 | 78 | assert l.bopt('not', 'res4', 'a1') == 91 79 | assert l.bcount('res4') == 86 80 | 81 | def test_bexpire(self): 82 | assert not l.bexpire('a', 100) 83 | l.bsetbit('a', 1, True) 84 | assert l.bexpire('a', 100) 85 | assert 0 < l.bttl('a') <= 100 86 | assert l.bpersist('a') 87 | assert l.bttl('a') == -1 88 | 89 | def test_bexpireat_datetime(self): 90 | l.bsetbit('a', 1, True) 91 | assert l.bexpireat('a', expire_at()) 92 | assert 0 < l.bttl('a') <= 61 93 | 94 | def test_bexpireat_unixtime(self): 95 | l.bsetbit('a', 1, True) 96 | assert l.bexpireat('a', expire_at_seconds()) 97 | assert 0 < l.bttl('a') <= 61 98 | 99 | def test_bexpireat_no_key(self): 100 | assert not l.bexpireat('a', expire_at()) 101 | 102 | def test_bttl_and_bpersist(self): 103 | l.bsetbit('a', 1, True) 104 | l.bexpire('a', 100) 105 | assert 0 < l.bttl('a') <= 100 106 | assert l.bpersist('a') 107 | assert l.bttl('a') == -1 108 | -------------------------------------------------------------------------------- /tests/test_cmd_hash.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for hash commands 3 | 4 | import unittest 5 | import sys 6 | 7 | sys.path.append('..') 8 | 9 | import ledis 10 | from ledis._compat import itervalues 11 | from util import expire_at, expire_at_seconds 12 | 13 | 14 | l = ledis.Ledis(port=6380) 15 | 16 | 17 | class TestCmdHash(unittest.TestCase): 18 | def setUp(self): 19 | pass 20 | 21 | def tearDown(self): 22 | l.flushdb() 23 | 24 | 25 | def test_hdel(self): 26 | l.hset('myhash', 'field1', 'foo') 27 | assert l.hdel('myhash', 'field1') == 1 28 | assert l.hdel('myhash', 'field1') == 0 29 | assert l.hdel('myhash', 'field1', 'field2') == 0 30 | 31 | def test_hexists(self): 32 | l.hset('myhash', 'field1', 'foo') 33 | l.hdel('myhash', 'field2') 34 | assert l.hexists('myhash', 'field1') == 1 35 | assert l.hexists('myhash', 'field2') == 0 36 | 37 | def test_hget(self): 38 | l.hset('myhash', 'field1', 'foo') 39 | assert l.hget('myhash', 'field1') == 'foo' 40 | assert (l.hget('myhash', 'field2')) is None 41 | 42 | def test_hgetall(self): 43 | h = {'field1': 'foo', 'field2': 'bar'} 44 | l.hmset('myhash', h) 45 | assert l.hgetall('myhash') == h 46 | 47 | def test_hincrby(self): 48 | assert l.hincrby('myhash', 'field1') == 1 49 | l.hclear('myhash') 50 | assert l.hincrby('myhash', 'field1', 1) == 1 51 | assert l.hincrby('myhash', 'field1', 5) == 6 52 | assert l.hincrby('myhash', 'field1', -10) == -4 53 | 54 | def test_hkeys(self): 55 | h = {'field1': 'foo', 'field2': 'bar'} 56 | l.hmset('myhash', h) 57 | assert l.hkeys('myhash') == ['field1', 'field2'] 58 | 59 | def test_hlen(self): 60 | l.hset('myhash', 'field1', 'foo') 61 | assert l.hlen('myhash') == 1 62 | l.hset('myhash', 'field2', 'bar') 63 | assert l.hlen('myhash') == 2 64 | 65 | 66 | def test_hmget(self): 67 | assert l.hmset('myhash', {'a': '1', 'b': '2', 'c': '3'}) 68 | assert l.hmget('myhash', 'a', 'b', 'c') == ['1', '2', '3'] 69 | 70 | 71 | def test_hmset(self): 72 | h = {'a': '1', 'b': '2', 'c': '3'} 73 | assert l.hmset('myhash', h) 74 | assert l.hgetall('myhash') == h 75 | 76 | def test_hset(self): 77 | l.hclear('myhash') 78 | assert int(l.hset('myhash', 'field1', 'foo')) == 1 79 | assert l.hset('myhash', 'field1', 'foo') == 0 80 | 81 | def test_hvals(self): 82 | h = {'a': '1', 'b': '2', 'c': '3'} 83 | l.hmset('myhash', h) 84 | local_vals = list(itervalues(h)) 85 | remote_vals = l.hvals('myhash') 86 | assert sorted(local_vals) == sorted(remote_vals) 87 | 88 | 89 | def test_hclear(self): 90 | h = {'a': '1', 'b': '2', 'c': '3'} 91 | l.hmset('myhash', h) 92 | assert l.hclear('myhash') == 3 93 | assert l.hclear('myhash') == 0 94 | 95 | 96 | def test_hmclear(self): 97 | h = {'a': '1', 'b': '2', 'c': '3'} 98 | l.hmset('myhash1', h) 99 | l.hmset('myhash2', h) 100 | assert l.hmclear('myhash1', 'myhash2') == 2 101 | 102 | 103 | def test_hexpire(self): 104 | assert l.hexpire('myhash', 100) == 0 105 | l.hset('myhash', 'field1', 'foo') 106 | assert l.hexpire('myhash', 100) == 1 107 | assert l.httl('myhash') <= 100 108 | 109 | def test_hexpireat_datetime(self): 110 | l.hset('a', 'f', 'foo') 111 | assert l.hexpireat('a', expire_at()) 112 | assert 0 < l.httl('a') <= 61 113 | 114 | def test_hexpireat_unixtime(self): 115 | l.hset('a', 'f', 'foo') 116 | assert l.hexpireat('a', expire_at_seconds()) 117 | assert 0 < l.httl('a') <= 61 118 | 119 | def test_hexpireat_no_key(self): 120 | assert not l.hexpireat('a', expire_at()) 121 | 122 | def test_hexpireat(self): 123 | assert l.hexpireat('myhash', 1577808000) == 0 124 | l.hset('myhash', 'field1', 'foo') 125 | assert l.hexpireat('myhash', 1577808000) == 1 126 | 127 | def test_httl(self): 128 | l.hset('myhash', 'field1', 'foo') 129 | assert l.hexpire('myhash', 100) 130 | assert l.httl('myhash') <= 100 131 | 132 | def test_hpersist(self): 133 | l.hset('myhash', 'field1', 'foo') 134 | l.hexpire('myhash', 100) 135 | assert l.httl('myhash') <= 100 136 | assert l.hpersist('myhash') 137 | assert l.httl('myhash') == -1 138 | 139 | -------------------------------------------------------------------------------- /tests/test_cmd_kv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for k/v commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b, iteritems 10 | from util import expire_at, expire_at_seconds 11 | 12 | 13 | l = ledis.Ledis(port=6380) 14 | 15 | 16 | class TestCmdKv(unittest.TestCase): 17 | def setUp(self): 18 | pass 19 | 20 | def tearDown(self): 21 | l.flushdb() 22 | 23 | def test_decr(self): 24 | assert l.delete('a') == 1 25 | assert l.decr('a') == -1 26 | assert l['a'] == b('-1') 27 | assert l.decr('a') == -2 28 | assert l['a'] == b('-2') 29 | assert l.decr('a', amount=5) == -7 30 | assert l['a'] == b('-7') 31 | 32 | def test_decrby(self): 33 | assert l.delete('a') == 1 34 | assert l.decrby('a') == -1 35 | assert l['a'] == b('-1') 36 | assert l.decrby('a') == -2 37 | assert l['a'] == b('-2') 38 | assert l.decrby('a', amount=5) == -7 39 | assert l['a'] == b('-7') 40 | 41 | def test_del(self): 42 | assert l.delete('a') == 1 43 | assert l.delete('a', 'b', 'c') == 3 44 | 45 | def test_exists(self): 46 | l.delete('a', 'non_exist_key') 47 | l.set('a', 'hello') 48 | assert (l.exists('a')) 49 | assert not (l.exists('non_exist_key')) 50 | 51 | def test_get(self): 52 | l.set('a', 'hello') 53 | assert l.get('a') == 'hello' 54 | l.set('b', '中文') 55 | assert l.get('b') == '中文' 56 | l.delete('non_exist_key') 57 | assert (l.get('non_exist_key')) is None 58 | 59 | def test_getset(self): 60 | l.set('a', 'hello') 61 | assert l.getset('a', 'world') == 'hello' 62 | assert l.get('a') == 'world' 63 | l.delete('non_exist_key') 64 | assert (l.getset('non_exist_key', 'non')) is None 65 | 66 | def test_incr(self): 67 | l.delete('non_exist_key') 68 | assert l.incr('non_exist_key') == 1 69 | l.set('a', 100) 70 | assert l.incr('a') == 101 71 | 72 | def test_incrby(self): 73 | l.delete('a') 74 | assert l.incrby('a', 100) == 100 75 | 76 | l.set('a', 100) 77 | assert l.incrby('a', 100) == 200 78 | assert l.incrby('a', amount=100) == 300 79 | 80 | def test_mget(self): 81 | l.set('a', 'hello') 82 | l.set('b', 'world') 83 | l.delete('non_exist_key') 84 | assert l.mget('a', 'b', 'non_exist_key') == ['hello', 'world', None] 85 | l.delete('a', 'b') 86 | assert l.mget(['a', 'b']) == [None, None] 87 | 88 | def test_mset(self): 89 | d = {'a': b('1'), 'b': b('2'), 'c': b('3')} 90 | assert l.mset(**d) 91 | for k, v in iteritems(d): 92 | assert l[k] == v 93 | 94 | def test_set(self): 95 | assert (l.set('a', 100)) 96 | 97 | def test_setnx(self): 98 | l.delete('a') 99 | assert l.setnx('a', '1') 100 | assert l['a'] == b('1') 101 | assert not l.setnx('a', '2') 102 | assert l['a'] == b('1') 103 | 104 | def test_ttl(self): 105 | assert l.set('a', 'hello') 106 | assert l.expire('a', 100) 107 | assert l.ttl('a') <= 100 108 | l.delete('a') 109 | assert l.ttl('a') == -1 110 | l.set('a', 'hello') 111 | assert l.ttl('a') == -1 112 | 113 | def test_setex(self): 114 | l.delete('a') 115 | assert l.setex('a', 10, '1') 116 | assert l.ttl('a') == 10 117 | assert l['a'] == b('1') 118 | 119 | def test_persist(self): 120 | assert l.set('a', 'hello') 121 | assert l.expire('a', 100) 122 | assert l.ttl('a') <= 100 123 | assert l.persist('a') 124 | l.delete('non_exist_key') 125 | assert not l.persist('non_exist_key') 126 | 127 | def test_expire(self): 128 | assert not l.expire('a', 100) 129 | 130 | l.set('a', 'hello') 131 | assert (l.expire('a', 100)) 132 | l.delete('a') 133 | assert not (l.expire('a', 100)) 134 | 135 | def test_expireat_datetime(self): 136 | l.set('a', '1') 137 | assert l.expireat('a', expire_at()) 138 | assert 0 < l.ttl('a') <= 61 139 | 140 | def test_expireat_unixtime(self): 141 | l.set('a', '1') 142 | assert l.expireat('a', expire_at_seconds()) 143 | assert 0 < l.ttl('a') <= 61 144 | 145 | def test_expireat_no_key(self): 146 | assert not l.expireat('a', expire_at()) 147 | 148 | def test_expireat(self): 149 | l.set('a', 'hello') 150 | assert (l.expireat('a', 1577808000)) # time is 2020.1.1 151 | l.delete('a') 152 | assert not(l.expireat('a', 1577808000)) 153 | -------------------------------------------------------------------------------- /tests/test_cmd_list.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for list commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b 10 | from util import expire_at, expire_at_seconds 11 | 12 | 13 | l = ledis.Ledis(port=6380) 14 | 15 | 16 | class TestCmdList(unittest.TestCase): 17 | def setUp(self): 18 | pass 19 | 20 | def tearDown(self): 21 | l.flushdb() 22 | 23 | def test_lindex(self): 24 | l.rpush('mylist', '1', '2', '3') 25 | assert l.lindex('mylist', 0) == b('1') 26 | assert l.lindex('mylist', 1) == b('2') 27 | assert l.lindex('mylist', 2) == b('3') 28 | 29 | def test_llen(self): 30 | l.rpush('mylist', '1', '2', '3') 31 | assert l.llen('mylist') == 3 32 | 33 | def test_lpop(self): 34 | l.rpush('mylist', '1', '2', '3') 35 | assert l.lpop('mylist') == b('1') 36 | assert l.lpop('mylist') == b('2') 37 | assert l.lpop('mylist') == b('3') 38 | assert l.lpop('mylist') is None 39 | 40 | def test_lpush(self): 41 | assert l.lpush('mylist', '1') == 1 42 | assert l.lpush('mylist', '2') == 2 43 | assert l.lpush('mylist', '3', '4', '5') == 5 44 | assert l.lrange('mylist', 0, -1) == ['5', '4', '3', '2', '1'] 45 | 46 | def test_lrange(self): 47 | l.rpush('mylist', '1', '2', '3', '4', '5') 48 | assert l.lrange('mylist', 0, 2) == ['1', '2', '3'] 49 | assert l.lrange('mylist', 2, 10) == ['3', '4', '5'] 50 | assert l.lrange('mylist', 0, -1) == ['1', '2', '3', '4', '5'] 51 | 52 | def test_rpush(self): 53 | assert l.rpush('mylist', '1') == 1 54 | assert l.rpush('mylist', '2') == 2 55 | assert l.rpush('mylist', '3', '4') == 4 56 | assert l.lrange('mylist', 0, -1) == ['1', '2', '3', '4'] 57 | 58 | def test_rpop(self): 59 | l.rpush('mylist', '1', '2', '3') 60 | assert l.rpop('mylist') == b('3') 61 | assert l.rpop('mylist') == b('2') 62 | assert l.rpop('mylist') == b('1') 63 | assert l.rpop('mylist') is None 64 | 65 | def test_lclear(self): 66 | l.rpush('mylist', '1', '2', '3') 67 | assert l.lclear('mylist') == 3 68 | assert l.lclear('mylist') == 0 69 | 70 | def test_lmclear(self): 71 | l.rpush('mylist1', '1', '2', '3') 72 | l.rpush('mylist2', '1', '2', '3') 73 | assert l.lmclear('mylist1', 'mylist2') == 2 74 | 75 | def test_lexpire(self): 76 | assert not l.lexpire('mylist', 100) 77 | l.rpush('mylist', '1') 78 | assert l.lexpire('mylist', 100) 79 | assert 0 < l.lttl('mylist') <= 100 80 | assert l.lpersist('mylist') 81 | assert l.lttl('mylist') == -1 82 | 83 | def test_lexpireat_datetime(self): 84 | l.rpush('mylist', '1') 85 | assert l.lexpireat('mylist', expire_at()) 86 | assert 0 < l.lttl('mylist') <= 61 87 | 88 | def test_lexpireat_unixtime(self): 89 | l.rpush('mylist', '1') 90 | assert l.lexpireat('mylist', expire_at_seconds()) 91 | assert l.lttl('mylist') <= 61 92 | 93 | def test_lexpireat_no_key(self): 94 | assert not l.lexpireat('mylist', expire_at()) 95 | 96 | def test_lttl_and_lpersist(self): 97 | l.rpush('mylist', '1') 98 | l.lexpire('mylist', 100) 99 | assert 0 < l.lttl('mylist') <= 100 100 | assert l.lpersist('mylist') 101 | assert l.lttl('mylist') == -1 102 | 103 | -------------------------------------------------------------------------------- /tests/test_cmd_script.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for bit commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b 10 | from util import expire_at, expire_at_seconds 11 | 12 | l = ledis.Ledis(port=6380) 13 | 14 | 15 | simple_script = "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 16 | 17 | 18 | class TestCmdScript(unittest.TestCase): 19 | def setUp(self): 20 | pass 21 | 22 | def tearDown(self): 23 | l.flushdb() 24 | 25 | def test_eval(self): 26 | assert l.eval(simple_script, ["key1", "key2"], "first", "second") == ["key1", "key2", "first", "second"] 27 | 28 | def test_evalsha(self): 29 | sha1 = l.scriptload(simple_script) 30 | assert len(sha1) == 40 31 | 32 | assert l.evalsha(sha1, ["key1", "key2"], "first", "second") == ["key1", "key2", "first", "second"] 33 | 34 | def test_scriptload(self): 35 | sha1 = l.scriptload(simple_script) 36 | assert len(sha1) == 40 37 | 38 | def test_scriptexists(self): 39 | sha1 = l.scriptload(simple_script) 40 | assert l.scriptexists(sha1) == [1L] 41 | 42 | def test_scriptflush(self): 43 | sha1 = l.scriptload(simple_script) 44 | assert l.scriptexists(sha1) == [1L] 45 | assert l.scriptflush() == 'OK' 46 | 47 | assert l.scriptexists(sha1) == [0L] 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/test_cmd_set.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test set commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import pytest 9 | 10 | import ledis 11 | from ledis._compat import b 12 | from ledis import ResponseError 13 | from util import expire_at, expire_at_seconds 14 | 15 | l = ledis.Ledis(port=6380) 16 | 17 | 18 | class TestCmdSet(unittest.TestCase): 19 | def setUp(self): 20 | pass 21 | 22 | def tearDown(self): 23 | l.flushdb() 24 | 25 | def test_sadd(self): 26 | members = set([b('1'), b('2'), b('3')]) 27 | l.sadd('a', *members) 28 | assert l.smembers('a') == members 29 | 30 | def test_scard(self): 31 | l.sadd('a', '1', '2', '3') 32 | assert l.scard('a') == 3 33 | 34 | def test_sdiff(self): 35 | l.sadd('a', '1', '2', '3') 36 | assert l.sdiff('a', 'b') == set([b('1'), b('2'), b('3')]) 37 | l.sadd('b', '2', '3') 38 | assert l.sdiff('a', 'b') == set([b('1')]) 39 | 40 | def test_sdiffstore(self): 41 | l.sadd('a', '1', '2', '3') 42 | assert l.sdiffstore('c', 'a', 'b') == 3 43 | assert l.smembers('c') == set([b('1'), b('2'), b('3')]) 44 | l.sadd('b', '2', '3') 45 | print l.smembers('c') 46 | print "before" 47 | assert l.sdiffstore('c', 'a', 'b') == 1 48 | print l.smembers('c') 49 | assert l.smembers('c') == set([b('1')]) 50 | 51 | def test_sinter(self): 52 | l.sadd('a', '1', '2', '3') 53 | assert l.sinter('a', 'b') == set() 54 | l.sadd('b', '2', '3') 55 | assert l.sinter('a', 'b') == set([b('2'), b('3')]) 56 | 57 | def test_sinterstore(self): 58 | l.sadd('a', '1', '2', '3') 59 | assert l.sinterstore('c', 'a', 'b') == 0 60 | assert l.smembers('c') == set() 61 | l.sadd('b', '2', '3') 62 | assert l.sinterstore('c', 'a', 'b') == 2 63 | assert l.smembers('c') == set([b('2'), b('3')]) 64 | 65 | def test_sismember(self): 66 | l.sadd('a', '1', '2', '3') 67 | assert l.sismember('a', '1') 68 | assert l.sismember('a', '2') 69 | assert l.sismember('a', '3') 70 | assert not l.sismember('a', '4') 71 | 72 | def test_smembers(self): 73 | l.sadd('a', '1', '2', '3') 74 | assert l.smembers('a') == set([b('1'), b('2'), b('3')]) 75 | 76 | def test_srem(self): 77 | l.sadd('a', '1', '2', '3', '4') 78 | assert l.srem('a', '5') == 0 79 | assert l.srem('a', '2', '4') == 2 80 | assert l.smembers('a') == set([b('1'), b('3')]) 81 | 82 | def test_sunion(self): 83 | l.sadd('a', '1', '2') 84 | l.sadd('b', '2', '3') 85 | assert l.sunion('a', 'b') == set([b('1'), b('2'), b('3')]) 86 | 87 | def test_sunionstore(self): 88 | l.sadd('a', '1', '2') 89 | l.sadd('b', '2', '3') 90 | assert l.sunionstore('c', 'a', 'b') == 3 91 | assert l.smembers('c') == set([b('1'), b('2'), b('3')]) 92 | 93 | def test_sclear(self): 94 | members = set([b('1'), b('2'), b('3')]) 95 | l.sadd('a', *members) 96 | assert l.sclear('a') == 3 97 | assert l.sclear('a') == 0 98 | 99 | def test_smclear(self): 100 | members = set([b('1'), b('2'), b('3')]) 101 | l.sadd('a', *members) 102 | l.sadd('b', *members) 103 | assert l.smclear('a', 'b') == 2 104 | 105 | def test_sexpire(self): 106 | members = set([b('1'), b('2'), b('3')]) 107 | assert l.sexpire('a', 100) == 0 108 | l.sadd('a', *members) 109 | assert l.sexpire('a', 100) == 1 110 | assert l.sttl('a') <= 100 111 | 112 | def test_sexpireat_datetime(self): 113 | members = set([b('1'), b('2'), b('3')]) 114 | l.sadd('a', *members) 115 | assert l.sexpireat('a', expire_at()) 116 | assert 0 < l.sttl('a') <= 61 117 | 118 | def test_sexpireat_unixtime(self): 119 | members = set([b('1'), b('2'), b('3')]) 120 | l.sadd('a', *members) 121 | assert l.sexpireat('a', expire_at_seconds()) 122 | assert 0 < l.sttl('a') <= 61 123 | 124 | def test_sexpireat_no_key(self): 125 | assert not l.sexpireat('a', expire_at()) 126 | 127 | def test_sexpireat(self): 128 | assert l.sexpireat('a', 1577808000) == 0 129 | members = set([b('1'), b('2'), b('3')]) 130 | l.sadd('a', *members) 131 | assert l.sexpireat('a', 1577808000) == 1 132 | 133 | def test_sttl(self): 134 | members = set([b('1'), b('2'), b('3')]) 135 | l.sadd('a', *members) 136 | assert l.sexpire('a', 100) 137 | assert l.sttl('a') <= 100 138 | 139 | def test_spersist(self): 140 | members = set([b('1'), b('2'), b('3')]) 141 | l.sadd('a', *members) 142 | l.sexpire('a', 100) 143 | assert l.sttl('a') <= 100 144 | assert l.spersist('a') 145 | assert l.sttl('a') == -1 146 | 147 | def test_invalid_params(self): 148 | with pytest.raises(ResponseError) as excinfo: 149 | l.sadd("a") 150 | assert excinfo.value.message == "invalid command param" 151 | 152 | def test_invalid_value(self): 153 | members = set([b('1'), b('2'), b('3')]) 154 | l.sadd('a', *members) 155 | self.assertRaises(ResponseError, lambda: l.sexpire('a', 'a')) 156 | 157 | -------------------------------------------------------------------------------- /tests/test_cmd_zset.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for zset commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b 10 | from util import expire_at, expire_at_seconds 11 | 12 | l = ledis.Ledis(port=6380) 13 | 14 | 15 | class TestCmdZset(unittest.TestCase): 16 | def setUp(self): 17 | pass 18 | 19 | def tearDown(self): 20 | l.flushdb() 21 | 22 | def test_zadd(self): 23 | l.zadd('a', a1=1, a2=2, a3=3) 24 | assert l.zrange('a', 0, -1) == [b('a1'), b('a2'), b('a3')] 25 | 26 | def test_zcard(self): 27 | l.zadd('a', a1=1, a2=2, a3=3) 28 | assert l.zcard('a') == 3 29 | 30 | def test_zcount(self): 31 | l.zadd('a', a1=1, a2=2, a3=3) 32 | assert l.zcount('a', '-inf', '+inf') == 3 33 | assert l.zcount('a', 1, 2) == 2 34 | assert l.zcount('a', 10, 20) == 0 35 | 36 | def test_zincrby(self): 37 | l.zadd('a', a1=1, a2=2, a3=3) 38 | assert l.zincrby('a', 'a2') == 3 39 | assert l.zincrby('a', 'a3', amount=5) == 8 40 | assert l.zscore('a', 'a2') == 3 41 | assert l.zscore('a', 'a3') == 8 42 | 43 | def test_zrange(self): 44 | l.zadd('a', a1=1, a2=2, a3=3) 45 | assert l.zrange('a', 0, 1) == [b('a1'), b('a2')] 46 | assert l.zrange('a', 2, 3) == [b('a3')] 47 | 48 | #withscores 49 | assert l.zrange('a', 0, 1, withscores=True) == \ 50 | [(b('a1'), 1), (b('a2'), 2)] 51 | assert l.zrange('a', 2, 3, withscores=True) == \ 52 | [(b('a3'), 3)] 53 | 54 | def test_zrangebyscore(self): 55 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 56 | assert l.zrangebyscore('a', 2, 4) == [b('a2'), b('a3'), b('a4')] 57 | 58 | # slicing with start/num 59 | assert l.zrangebyscore('a', 2, 4, start=1, num=2) == \ 60 | [b('a3'), b('a4')] 61 | 62 | # withscores 63 | assert l.zrangebyscore('a', 2, 4, withscores=True) == \ 64 | [('a2', 2), ('a3', 3), ('a4', 4)] 65 | 66 | def test_zrank(self): 67 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 68 | assert l.zrank('a', 'a1') == 0 69 | assert l.zrank('a', 'a3') == 2 70 | assert l.zrank('a', 'a6') is None 71 | 72 | def test_zrem(self): 73 | l.zadd('a', a1=1, a2=2, a3=3) 74 | assert l.zrem('a', 'a2') == 1 75 | assert l.zrange('a', 0, -1) == [b('a1'), b('a3')] 76 | assert l.zrem('a', 'b') == 0 77 | assert l.zrange('a', 0, -1) == [b('a1'), b('a3')] 78 | 79 | # multiple keys 80 | l.zadd('a', a1=1, a2=2, a3=3) 81 | assert l.zrem('a', 'a1', 'a2') == 2 82 | assert l.zrange('a', 0, -1) == [b('a3')] 83 | 84 | def test_zremrangebyrank(self): 85 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 86 | assert l.zremrangebyrank('a', 1, 3) == 3 87 | assert l.zrange('a', 0, -1) == [b('a1'), b('a5')] 88 | 89 | def test_zremrangebyscore(self): 90 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 91 | assert l.zremrangebyscore('a', 2, 4) == 3 92 | assert l.zrange('a', 0, -1) == [b('a1'), b('a5')] 93 | assert l.zremrangebyscore('a', 2, 4) == 0 94 | assert l.zrange('a', 0, -1) == [b('a1'), b('a5')] 95 | 96 | def test_zrevrange(self): 97 | l.zadd('a', a1=1, a2=2, a3=3) 98 | assert l.zrevrange('a', 0, 1) == [b('a3'), b('a2')] 99 | assert l.zrevrange('a', 1, 2) == [b('a2'), b('a1')] 100 | 101 | def test_zrevrank(self): 102 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 103 | assert l.zrevrank('a', 'a1') == 4 104 | assert l.zrevrank('a', 'a2') == 3 105 | assert l.zrevrank('a', 'a6') is None 106 | 107 | def test_zrevrangebyscore(self): 108 | l.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) 109 | assert l.zrevrangebyscore('a', 4, 2) == [b('a4'), b('a3'), b('a2')] 110 | 111 | # slicing with start/num 112 | assert l.zrevrangebyscore('a', 4, 2, start=1, num=2) == \ 113 | [b('a3'), b('a2')] 114 | 115 | # withscores 116 | assert l.zrevrangebyscore('a', 4, 2, withscores=True) == \ 117 | [(b('a4'), 4), (b('a3'), 3), (b('a2'), 2)] 118 | 119 | def test_zscore(self): 120 | l.zadd('a', a1=1, a2=2, a3=3) 121 | assert l.zscore('a', 'a1') == 1 122 | assert l.zscore('a', 'a2') == 2 123 | assert l.zscore('a', 'a4') is None 124 | 125 | def test_zclear(self): 126 | l.zadd('a', a1=1, a2=2, a3=3) 127 | assert l.zclear('a') == 3 128 | assert l.zclear('a') == 0 129 | 130 | def test_zmclear(self): 131 | l.zadd('a', a1=1, a2=2, a3=3) 132 | l.zadd('b', b1=1, b2=2, b3=3) 133 | assert l.lmclear('a', 'b') == 2 134 | assert l.lmclear('c', 'd') == 2 135 | 136 | def test_zexpire(self): 137 | assert not l.zexpire('a', 100) 138 | l.zadd('a', a1=1, a2=2, a3=3) 139 | assert l.zexpire('a', 100) 140 | assert 0 < l.zttl('a') <= 100 141 | assert l.zpersist('a') 142 | assert l.zttl('a') == -1 143 | 144 | def test_zexpireat_datetime(self): 145 | l.zadd('a', a1=1) 146 | assert l.zexpireat('a', expire_at()) 147 | assert 0 < l.zttl('a') <= 61 148 | 149 | def test_zexpireat_unixtime(self): 150 | l.zadd('a', a1=1) 151 | assert l.zexpireat('a', expire_at_seconds()) 152 | assert 0 < l.zttl('a') <= 61 153 | 154 | def test_zexpireat_no_key(self): 155 | assert not l.zexpireat('a', expire_at()) 156 | 157 | def test_zttl_and_zpersist(self): 158 | l.zadd('a', a1=1) 159 | l.zexpire('a', 100) 160 | assert 0 < l.zttl('a') <= 100 161 | assert l.zpersist('a') 162 | assert l.zttl('a') == -1 163 | -------------------------------------------------------------------------------- /tests/test_others.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Test Cases for other commands 3 | 4 | import unittest 5 | import sys 6 | sys.path.append('..') 7 | 8 | import ledis 9 | from ledis._compat import b 10 | from ledis import ResponseError 11 | 12 | l = ledis.Ledis(port=6380) 13 | dbs = ["leveldb", "rocksdb", "goleveldb", "hyperleveldb", "lmdb", "boltdb"] 14 | 15 | class TestOtherCommands(unittest.TestCase): 16 | def setUp(self): 17 | pass 18 | 19 | def tearDown(self): 20 | l.flushdb() 21 | 22 | # server information 23 | def test_echo(self): 24 | assert l.echo('foo bar') == b('foo bar') 25 | 26 | def test_ping(self): 27 | assert l.ping() 28 | 29 | def test_select(self): 30 | assert l.select('1') 31 | assert l.select('15') 32 | self.assertRaises(ResponseError, lambda: l.select('16')) 33 | 34 | 35 | def test_info(self): 36 | info1 = l.info() 37 | assert info1.get("db_name") in dbs 38 | info2 = l.info(section="server") 39 | assert info2.get("os") in ["linux", "darwin"] 40 | 41 | def test_flushdb(self): 42 | l.set("a", 1) 43 | assert l.flushdb() == "OK" 44 | assert l.get("a") is None 45 | 46 | def test_flushall(self): 47 | l.select(1) 48 | l.set("a", 1) 49 | assert l.get("a") == b("1") 50 | 51 | l.select(10) 52 | l.set("a", 1) 53 | assert l.get("a") == b("1") 54 | 55 | assert l.flushall() == "OK" 56 | 57 | assert l.get("a") is None 58 | l.select(1) 59 | assert l.get("a") is None 60 | 61 | 62 | # test *scan commands 63 | 64 | def check_keys(self, scan_type): 65 | d = { 66 | "xscan": l.xscan, 67 | "sxscan": l.sxscan, 68 | "lxscan": l.lxscan, 69 | "hxscan": l.hxscan, 70 | "zxscan": l.zxscan, 71 | "bxscan": l.bxscan 72 | } 73 | 74 | key, keys = d[scan_type]() 75 | assert key == "" 76 | assert set(keys) == set([b("a"), b("b"), b("c")]) 77 | 78 | _, keys = d[scan_type](match="a") 79 | assert set(keys) == set([b("a")]) 80 | 81 | _, keys = d[scan_type](key="a") 82 | assert set(keys) == set([b("b"), b("c")]) 83 | 84 | 85 | def test_xscan(self): 86 | d = {"a":1, "b":2, "c": 3} 87 | l.mset(d) 88 | self.check_keys("xscan") 89 | 90 | 91 | def test_lxscan(self): 92 | l.rpush("a", 1) 93 | l.rpush("b", 1) 94 | l.rpush("c", 1) 95 | self.check_keys("lxscan") 96 | 97 | 98 | def test_hxscan(self): 99 | l.hset("a", "hello", "world") 100 | l.hset("b", "hello", "world") 101 | l.hset("c", "hello", "world") 102 | self.check_keys("hxscan") 103 | 104 | def test_sxscan(self): 105 | l.sadd("a", 1) 106 | l.sadd("b", 2) 107 | l.sadd("c", 3) 108 | self.check_keys("sxscan") 109 | 110 | def test_zxscan(self): 111 | l.zadd("a", 1, "a") 112 | l.zadd("b", 1, "a") 113 | l.zadd("c", 1, "a") 114 | self.check_keys("zxscan") 115 | 116 | def test_bxscan(self): 117 | l.bsetbit("a", 1, 1) 118 | l.bsetbit("b", 1, 1) 119 | l.bsetbit("c", 1, 1) 120 | self.check_keys("bxscan") 121 | 122 | -------------------------------------------------------------------------------- /tests/test_tx.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | sys.path.append("..") 4 | 5 | import ledis 6 | 7 | global_l = ledis.Ledis() 8 | 9 | #db that do not support transaction 10 | dbs = ["leveldb", "rocksdb", "hyperleveldb", "goleveldb"] 11 | check = global_l.info().get("db_name") in dbs 12 | 13 | 14 | class TestTx(unittest.TestCase): 15 | def setUp(self): 16 | self.l = ledis.Ledis(port=6380) 17 | 18 | def tearDown(self): 19 | self.l.flushdb() 20 | 21 | @unittest.skipIf(check, reason="db not support transaction") 22 | def test_commit(self): 23 | tx = self.l.tx() 24 | self.l.set("a", "no-tx") 25 | assert self.l.get("a") == "no-tx" 26 | tx.begin() 27 | tx.set("a", "tx") 28 | assert self.l.get("a") == "no-tx" 29 | assert tx.get("a") == "tx" 30 | 31 | tx.commit() 32 | assert self.l.get("a") == "tx" 33 | 34 | @unittest.skipIf(check, reason="db not support transaction") 35 | def test_rollback(self): 36 | tx = self.l.tx() 37 | self.l.set("a", "no-tx") 38 | assert self.l.get("a") == "no-tx" 39 | 40 | tx.begin() 41 | tx.set("a", "tx") 42 | assert tx.get("a") == "tx" 43 | assert self.l.get("a") == "no-tx" 44 | 45 | tx.rollback() 46 | assert self.l.get("a") == "no-tx" -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import datetime 3 | import time 4 | 5 | 6 | def current_time(): 7 | return datetime.datetime.now() 8 | 9 | 10 | def expire_at(minute=1): 11 | expire_at = current_time() + datetime.timedelta(minutes=minute) 12 | return expire_at 13 | 14 | 15 | def expire_at_seconds(minute=1): 16 | return int(time.mktime(expire_at(minute=minute).timetuple())) 17 | 18 | if __name__ == "__main__": 19 | print expire_at() 20 | print expire_at_seconds() --------------------------------------------------------------------------------