├── .checkignore ├── .codeclimate.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bootstrap ├── docker-compose.yml ├── docker ├── redis-cluster │ ├── Dockerfile │ └── run.sh └── redis-trib │ ├── Dockerfile │ ├── create-cluster.sh │ ├── docker-compose.yml │ └── run.sh ├── docs ├── Makefile ├── api.rst ├── conf.py ├── example.rst ├── exceptions.rst ├── history.rst ├── index.rst └── supported-commands.rst ├── example.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests ├── __init__.py ├── base.py ├── cluster_behavior_tests.py ├── cluster_tests.py ├── connect_tests.py ├── crc16_tests.py ├── failover_tests.py ├── hash_tests.py ├── hyperloglog_tests.py ├── keys_tests.py ├── list_tests.py ├── scripting_tests.py ├── server_tests.py ├── sets_tests.py ├── sortedsets_tests.py └── strings_tests.py ├── tox.ini └── tredis ├── __init__.py ├── client.py ├── cluster.py ├── common.py ├── compat.py ├── connection.py ├── crc16.py ├── exceptions.py ├── geo.py ├── hashes.py ├── hyperloglog.py ├── keys.py ├── lists.py ├── pubsub.py ├── scripting.py ├── server.py ├── sets.py ├── sortedsets.py ├── strings.py └── transactions.py /.checkignore: -------------------------------------------------------------------------------- 1 | setup.py 2 | tests/*.py 3 | docs/*.py 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | exclude_paths: 4 | - tests.py 5 | - setup.py 6 | - docs/conf.py 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.py] 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [.travis.yml] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.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 | env2/ 12 | env3/ 13 | bin/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .coverage 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | 47 | # Rope 48 | .ropeproject 49 | 50 | # Django stuff: 51 | *.log 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | .idea 58 | .vagrant 59 | env-vars 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | services: 4 | - docker 5 | python: 6 | - 2.7 7 | - 3.4 8 | - 3.5 9 | - 3.6 10 | install: 11 | - pip install -r test-requirements.txt 12 | - pip install wheel 13 | before_script: 14 | - ./bootstrap 127.0.0.1 15 | - sleep 15 16 | script: 17 | - source build/test-environment 18 | - nosetests 19 | after_failure: 20 | - docker-compose -p tredis logs 21 | after_success: 22 | - codecov 23 | deploy: 24 | distributions: bdist_wheel sdist 25 | provider: pypi 26 | user: crad 27 | on: 28 | python: 2.7 29 | tags: true 30 | all_branches: true 31 | password: 32 | secure: "SiBwS2Ebl2+zMKrFMOW5VtpO3lOWEVK8+Jg+DMsGcNTmPEnoVWPz2qNAFpS0QWkkhqe4rhx/MBlrVt7N68WEAg4iAvLF9oCe837w56vQb+L8l8Opo496KyCrfyy5iVfDXmYrhsNRtOtyaj+b/14IcjOR6qFo07Iz/wirx7mhUO6/cC8e1iIes8B5sqmSwtZ9jy+I1881mEhILd+0TZwCAMITymLuqhg0shnE3VbydqGNs2H7uypqXemAq3pG7fMxGA0NRNskBlEAJqwuqiRKJeeWrD459uYcFXAqn5NkRYyLSSCIzTMeSL190qIJyVeJSPrVAdv9ZNeI1eRapwUc0H0wUJIHPEe2T+wyNCF499Yy9o6m7htnDeML2H62Qv0HNJUmI9KZvVfyv55Kjd16nP9AcIUAGuLzYjaphOGn42GWr2jiwG9Ujpik1rSaBQN79JEUxDyvZ9WLgPDgjeSUjatCDoeH/LcKohgpUZfQMXadkygb7ZsVMD+9VC5bAjM/06SCOdK4pUjxKCcaFPMUcduw6bHvxfGJLYah1YqlLWgrPhVX69a60irSxSps2w/nw/U37gmYCfpaRFAuVHIDl1bFpCHv5/7k2LSdrg1c+Oa49fkZ/wkpFHh0VTh6VEkNg0zGbgJdsn6jMvogmGTPyowstxlNDKZQU6GcuqDhzJU=" 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Local Development and Contributing 2 | ---------------------------------- 3 | The development environment for tredis uses `docker-compose `_ 4 | and `docker-machine `_ 5 | 6 | To get setup in the environment and run the tests, take the following steps: 7 | 8 | .. code:: bash 9 | 10 | virtualenv -p python3 env 11 | source env/bin/activate 12 | 13 | ./bootstrap 14 | source build/test-environment 15 | 16 | nosetests 17 | 18 | Please format your code contributions with the ``yapf`` formatter: 19 | 20 | .. code:: bash 21 | 22 | yapf -i --style=pep8 tredis/MODULE.py 23 | 24 | Pull requests that make changes or additions that are not covered by tests 25 | will likely be closed without review. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2015-2018, Gavin M. Roy 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TRedis 2 | ====== 3 | An asynchronous Redis client for Tornado 4 | 5 | |Version| |Status| |Coverage| 6 | 7 | Documentation is available at `tredis.readthedocs.io `_. 8 | 9 | Commands Implemented 10 | -------------------- 11 | TRedis is a work in progress and not all commands are implemented. The following 12 | list details each command category and the number of commands implemented in each. 13 | 14 | If you need functionality that is not yet implemented, follow the patterns for 15 | the category mixins that are complete and submit a PR! 16 | 17 | +--------------+----------+ 18 | | Category | Count | 19 | +==============+==========+ 20 | | Cluster | 2 of 20 | 21 | +--------------+----------+ 22 | | Connection | 5 of 5 | 23 | +--------------+----------+ 24 | | Geo | 0 of 6 | 25 | +--------------+----------+ 26 | | Hashes | 13 of 15 | 27 | +--------------+----------+ 28 | | HyperLogLog | 3 of 3 | 29 | +--------------+----------+ 30 | | Keys | 22 of 22 | 31 | +--------------+----------+ 32 | | Lists | 9 of 17 | 33 | +--------------+----------+ 34 | | Pub/Sub | 0 of 6 | 35 | +--------------+----------+ 36 | | Scripting | 6 of 6 | 37 | +--------------+----------+ 38 | | Server | 7 of 30 | 39 | +--------------+----------+ 40 | | Sets | 15 of 15 | 41 | +--------------+----------+ 42 | | Sorted Sets | 8 of 21 | 43 | +--------------+----------+ 44 | | Strings | 23 of 23 | 45 | +--------------+----------+ 46 | | Transactions | 0 of 5 | 47 | +--------------+----------+ 48 | 49 | For information on local development or contributing, see `CONTRIBUTING.rst `_ 50 | 51 | Example 52 | ------- 53 | 54 | .. code:: python 55 | 56 | import logging 57 | import pprint 58 | 59 | from tornado import gen, ioloop 60 | import tredis 61 | 62 | 63 | @gen.engine 64 | def run(): 65 | client = tredis.Client([{"host": "127.0.0.1", "port": 6379, "db": 0}], 66 | auto_connect=False) 67 | yield client.connect() 68 | value = yield client.info() 69 | pprint.pprint(value) 70 | ioloop.IOLoop.current().stop() 71 | 72 | if __name__ == '__main__': 73 | logging.basicConfig(level=logging.DEBUG) 74 | io_loop = ioloop.IOLoop.current() 75 | io_loop.add_callback(run) 76 | io_loop.start() 77 | 78 | 79 | .. |Version| image:: https://img.shields.io/pypi/v/tredis.svg? 80 | :target: https://pypi.python.org/pypi/tredis 81 | 82 | .. |Status| image:: https://img.shields.io/travis/gmr/tredis.svg? 83 | :target: https://travis-ci.org/gmr/tredis 84 | 85 | .. |Coverage| image:: https://img.shields.io/codecov/c/github/gmr/tredis.svg? 86 | :target: https://codecov.io/github/gmr/tredis?branch=master 87 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # NAME 4 | # bootstrap -- initialize the test container environment 5 | # 6 | # SYNOPSIS 7 | # bootstrap 8 | # 9 | DOCKER_IP=${DOCKER_IP:-$1} 10 | if [ -z "${DOCKER_IP}" ] 11 | then 12 | if test -e /var/run/docker.sock 13 | then 14 | DOCKER_IP=127.0.0.1 15 | fi 16 | fi 17 | 18 | test -d build || mkdir build 19 | 20 | COMPOSE_ARGS="-p tredis" 21 | 22 | get_exposed_port() { # SERVICE PUBLIC-PORT [INDEX [PROTOCOL]] 23 | if test -n "$3" 24 | then 25 | index="--index=$3" 26 | fi 27 | if test -n "$4" 28 | then 29 | proto="--protocol=$4" 30 | fi 31 | port=$(docker-compose $COMPOSE_ARGS port $index $proto $1 $2 | cut -d: -f2) 32 | if test -z "$port" 33 | then 34 | exit 1 35 | fi 36 | echo $port 37 | } 38 | 39 | docker-compose ${COMPOSE_ARGS} stop 40 | docker-compose ${COMPOSE_ARGS} rm --force 41 | docker-compose ${COMPOSE_ARGS} up -d redis 42 | docker-compose ${COMPOSE_ARGS} scale redis=2 43 | 44 | REDIS1=$(docker inspect --format '{{ .NetworkSettings.Networks.tredis_default.IPAddress }}' tredis_redis_1) 45 | REDIS2=$(docker inspect --format '{{ .NetworkSettings.Networks.tredis_default.IPAddress }}' tredis_redis_2) 46 | 47 | echo "Making REDIS2 a slave of REDIS1: " 48 | COMMAND="redis-cli SLAVEOF ${REDIS1} 6379" 49 | docker exec -t -i tredis_redis_2 ${COMMAND} 50 | 51 | docker-compose ${COMPOSE_ARGS} up -d node1 node2 node3 52 | 53 | NODE1=$(docker inspect --format '{{ .NetworkSettings.Networks.tredis_default.IPAddress }}' tredis_node1_1) 54 | NODE2=$(docker inspect --format '{{ .NetworkSettings.Networks.tredis_default.IPAddress }}' tredis_node2_1) 55 | NODE3=$(docker inspect --format '{{ .NetworkSettings.Networks.tredis_default.IPAddress }}' tredis_node3_1) 56 | 57 | # Create a cluster 58 | echo "Creating a redis cluster with 3 nodes" 59 | COMMAND="create --replicas 0 ${NODE1}:6700 ${NODE2}:6701 ${NODE3}:6702" 60 | docker run --network tredis_default --rm -t -i gavinmroy/redis-trib:latest ${COMMAND} 61 | 62 | cat > build/test-environment < /etc/gemrc && \ 9 | gem install redis && \ 10 | curl -o /usr/local/bin/redis-trib.rb https://raw.githubusercontent.com/antirez/redis/3.2/src/redis-trib.rb && \ 11 | chmod a+x /usr/local/bin/redis-trib.rb && \ 12 | apk --purge del curl && \ 13 | rm -rf /var/cache/apk/* 14 | ADD run.sh /usr/local/bin/ 15 | 16 | ENTRYPOINT ["/usr/local/bin/run.sh"] 17 | -------------------------------------------------------------------------------- /docker/redis-trib/create-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | COMPOSE_ARGS="-p redis" 3 | 4 | docker-compose ${COMPOSE_ARGS} stop 5 | docker-compose ${COMPOSE_ARGS} rm --force 6 | docker-compose ${COMPOSE_ARGS} up -d 7 | 8 | NODE1=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node1_1) 9 | NODE2=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node2_1) 10 | NODE3=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node3_1) 11 | NODE4=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node4_1) 12 | NODE5=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node5_1) 13 | NODE6=$(docker inspect --format '{{ .NetworkSettings.Networks.redis_default.IPAddress }}' redis_node6_1) 14 | 15 | echo "create --replicas 1 ${NODE1}:6700 ${NODE2}:6701 ${NODE3}:6702 ${NODE4}:6703 ${NODE5}:6704 ${NODE6}:6705" 16 | 17 | COMMAND="create --replicas 1 ${NODE1}:6700 ${NODE2}:6701 ${NODE3}:6702 ${NODE4}:6703 ${NODE5}:6704 ${NODE6}:6705" 18 | 19 | docker run --network redis_default --rm -t -i gavinmroy/redis-trib:latest ${COMMAND} 20 | -------------------------------------------------------------------------------- /docker/redis-trib/docker-compose.yml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | version: '2' 4 | services: 5 | node1: 6 | image: gavinmroy/alpine-redis-cluster:3.2.5 7 | ports: 8 | - 6700:6700 9 | environment: 10 | - REDIS_PORT=6700 11 | node2: 12 | image: gavinmroy/alpine-redis-cluster:3.2.5 13 | ports: 14 | - 6701:6701 15 | environment: 16 | - REDIS_PORT=6701 17 | node3: 18 | image: gavinmroy/alpine-redis-cluster:3.2.5 19 | ports: 20 | - 6702:6702 21 | environment: 22 | - REDIS_PORT=6702 23 | node4: 24 | image: gavinmroy/alpine-redis-cluster:3.2.5 25 | ports: 26 | - 6703:6703 27 | environment: 28 | - REDIS_PORT=6703 29 | node5: 30 | image: gavinmroy/alpine-redis-cluster:3.2.5 31 | ports: 32 | - 6704:6704 33 | environment: 34 | - REDIS_PORT=6704 35 | node6: 36 | image: gavinmroy/alpine-redis-cluster:3.2.5 37 | ports: 38 | - 6705:6705 39 | environment: 40 | - REDIS_PORT=6705 41 | -------------------------------------------------------------------------------- /docker/redis-trib/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | echo "YES" | /usr/local/bin/redis-trib.rb $@ 3 | -------------------------------------------------------------------------------- /docs/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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TRedis.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TRedis.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/TRedis" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TRedis" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | The :py:class:`~tredis.Client` class is the primary API interface for 4 | interacting with Redis. While the per-method documentation attempts to be as 5 | complete as possible, the best documentation source for each Redis command is 6 | available `on the redis site `_. 7 | 8 | See the :doc:`supported-commands` documentation if you are not able to find 9 | a Redis command you are looking for. 10 | 11 | .. autoclass:: tredis.Client 12 | :members: 13 | :inherited-members: 14 | 15 | .. autoclass:: tredis.cluster.ClusterNode 16 | 17 | .. autoclass:: tredis.RedisClient 18 | :members: 19 | :inherited-members: 20 | 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | 4 | sys.path.insert(0, '../') 5 | 6 | import tredis 7 | 8 | 9 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.viewcode', 10 | 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx'] 11 | templates_path = ['_templates'] 12 | 13 | source_suffix = '.rst' 14 | master_doc = 'index' 15 | 16 | project = 'TRedis' 17 | copyright = '2015-{}, Gavin M. Roy'.format(datetime.date.today().year) 18 | 19 | release = tredis.__version__ 20 | version = '.'.join(release.split('.')[0:1]) 21 | exclude_patterns = ['_build'] 22 | pygments_style = 'sphinx' 23 | 24 | intersphinx_mapping = {'tornado': ('http://www.tornadoweb.org/en/stable/', None), 25 | 'python': ('https://docs.python.org/3/', None)} 26 | 27 | html_static_path = [] 28 | #autodoc_member_order = 'bysource' 29 | -------------------------------------------------------------------------------- /docs/example.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | The following examples expect a pre-existing asynchronous application: 4 | 5 | .. code-block:: python 6 | :caption: A simple set and get of a key from Redis 7 | 8 | import logging 9 | import pprint 10 | 11 | from tornado import gen, ioloop 12 | import tredis 13 | 14 | 15 | @gen.engine 16 | def run(): 17 | client = tredis.Client([{"host": "127.0.0.1", "port": 6379, "db": 0}], 18 | auto_connect=False) 19 | yield client.connect() 20 | yield client.set("foo", "bar") 21 |        value = yield client.get("foo") 22 | pprint.pprint(value) 23 | ioloop.IOLoop.current().stop() 24 | 25 | if __name__ == '__main__': 26 | logging.basicConfig(level=logging.DEBUG) 27 | io_loop = ioloop.IOLoop.current() 28 | io_loop.add_callback(run) 29 | io_loop.start() 30 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. autoclass:: tredis.exceptions.TRedisException 5 | 6 | .. autoclass:: tredis.exceptions.ConnectError 7 | 8 | .. autoclass:: tredis.exceptions.ConnectionError 9 | 10 | .. autoclass:: tredis.exceptions.InvalidClusterCommand 11 | 12 | .. autoclass:: tredis.exceptions.AuthError 13 | 14 | .. autoclass:: tredis.exceptions.RedisError 15 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | Version History 2 | =============== 3 | 4 | - 0.8.0 - released *2018-07-20* 5 | 6 | - Add `List `_ commands (9 of 17) (#7 - dave-shawley) 7 | - Add :meth:`~tredis.Client.zcard` (#8 - ibnpaul) 8 | - Add :meth:`~tredis.Client.zscore` (#8 - ibnpaul) 9 | - Documentation fixes (#6 - Zephor5) 10 | 11 | - 0.7.0 - released *2017-02-03* 12 | 13 | - Add :meth:`~tredis.Client.zrange` 14 | - Add :meth:`~tredis.Client.zrevrange` 15 | 16 | - 0.7.0 - released *2017-02-02* 17 | 18 | - Add support for Redis Clusters in the new :class:`~tredis.Client` class 19 | - Add :meth:`~tredis.Client.cluster_info` and :meth:`~tredis.Client.cluster_nodes` 20 | 21 | - 0.6.0 - released *2017-01-27* 22 | 23 | - Add :meth:`~tredis.Client.zrem` to the `Sorted Sets `_ commands 24 | - Locate master and reconnect when a ``READONLY`` response is received 25 | - Add :meth:`~tredis.Client.time` command 26 | 27 | - 0.5.0 - released *2016-11-08* 28 | 29 | - Add `Hash `_ commands (13 of 15) 30 | - Add `Sorted Sets `_ commands (3 of 21) 31 | 32 | - 0.4.0 - released *2016-01-25* 33 | 34 | - Add :class:`~tredis.Client.info` command 35 | 36 | - 0.3.0 - released *2016-01-18* 37 | 38 | - Remove broken pipelining implementation 39 | - Add scripting commands 40 | 41 | - 0.2.1 - released *2015-11-23* 42 | 43 | - Add hiredis to the requirements 44 | 45 | - 0.2.0 - released *2015-11-23* 46 | 47 | - Add per-command execution locking, preventing errors with concurrency in command processing 48 | - Clean up connection logic to simplify connecting to exist within the command execution lock instead of maintaining its own event 49 | - Add all missing methods in the strings category 50 | - Add hyperloglog methods 51 | - Add support for mixins to extend core tredis.RedisClient methods in future versions 52 | - Significant updates to docstrings 53 | 54 | - 0.1.0 - released *2015-11-20* 55 | 56 | - initial version 57 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | TRedis 2 | ====== 3 | An asynchronous Redis client for Tornado 4 | 5 | |Version| |License| 6 | 7 | .. note:: ``TRedis`` is a work in progress and does not support the entire Redis 8 | command set. For a list of the currently supported commands by category, see 9 | the :doc:`supported-commands` documentation. 10 | 11 | Installation 12 | ------------ 13 | tredis is available from the `Python Package Index `_ and can be installed by running :command:`pip install tredis` 14 | 15 | Contents 16 | -------- 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | api 22 | exceptions 23 | supported-commands 24 | example 25 | history 26 | 27 | Issues 28 | ------ 29 | Please report any issues to the Github repo at `https://github.com/gmr/tredis/issues `_ 30 | 31 | Source 32 | ------ 33 | TRedis source is available on Github at `https://github.com/gmr/tredis `_ 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | 42 | .. |Version| image:: https://img.shields.io/pypi/v/tredis.svg? 43 | :target: https://pypi.python.org/pypi/tredis 44 | 45 | .. |License| image:: https://img.shields.io/github/license/gmr/tredis.svg? 46 | :target: https://github.com/gmr/tredis 47 | -------------------------------------------------------------------------------- /docs/supported-commands.rst: -------------------------------------------------------------------------------- 1 | Supported Commands 2 | ================== 3 | The following table summarizes the number of commands supported by category: 4 | 5 | +--------------+----------+---------------+ 6 | | Category | Count | Version Added | 7 | +==============+==========+===============+ 8 | | Cluster | 2 of 20 | 0.7.0 | 9 | +--------------+----------+---------------+ 10 | | Connection | 5 of 5 | 0.1.0 | 11 | +--------------+----------+---------------+ 12 | | Geo | 0 of 6 | — | 13 | +--------------+----------+---------------+ 14 | | Hashes | 13 of 15 | 0.4.0 | 15 | +--------------+----------+---------------+ 16 | | HyperLogLog | 3 of 3 | 0.2.0 | 17 | +--------------+----------+---------------+ 18 | | Keys | 22 of 22 | 0.1.0 | 19 | +--------------+----------+---------------+ 20 | | Lists | 9 of 17 | 0.8.0 | 21 | +--------------+----------+---------------+ 22 | | Pub/Sub | 0 of 6 | - | 23 | +--------------+----------+---------------+ 24 | | Scripting | 6 of 6 | 0.3.0 | 25 | +--------------+----------+---------------+ 26 | | Server | 7 of 30 | 0.1.0+ | 27 | +--------------+----------+---------------+ 28 | | Sets | 15 of 15 | 0.1.0 | 29 | +--------------+----------+---------------+ 30 | | Sorted Sets | 8 of 21 | 0.4.0+ | 31 | +--------------+----------+---------------+ 32 | | Strings | 23 of 23 | 0.2.0 | 33 | +--------------+----------+---------------+ 34 | | Transactions | 0 of 5 | — | 35 | +--------------+----------+---------------+ 36 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pprint 3 | 4 | from tornado import gen, ioloop 5 | import tredis 6 | 7 | 8 | @gen.engine 9 | def run(): 10 | client = tredis.Client([{"host": "127.0.0.1", "port": 6379, "db": 0}], 11 | auto_connect=False) 12 | yield client.connect() 13 | value = yield client.info() 14 | pprint.pprint(value) 15 | ioloop.IOLoop.current().stop() 16 | 17 | if __name__ == '__main__': 18 | logging.basicConfig(level=logging.DEBUG) 19 | io_loop = ioloop.IOLoop.current() 20 | io_loop.add_callback(run) 21 | io_loop.start() 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado>4.0,<5 2 | hiredis>=0.2.0,<1 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-coverage=1 3 | cover-package=tredis 4 | cover-branches=1 5 | cover-erase=1 6 | verbosity=2 7 | cover-html = 1 8 | cover-html-dir = build/coverage 9 | 10 | [wheel] 11 | universal=1 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | 4 | import setuptools 5 | 6 | classifiers = ['Development Status :: 5 - Production/Stable', 7 | 'Intended Audience :: Developers', 8 | 'License :: OSI Approved :: BSD License', 9 | 'Operating System :: OS Independent', 10 | 'Programming Language :: Python :: 2', 11 | 'Programming Language :: Python :: 2.7', 12 | 'Programming Language :: Python :: 3', 13 | 'Programming Language :: Python :: 3.4', 14 | 'Programming Language :: Python :: 3.5', 15 | 'Programming Language :: Python :: 3.6', 16 | 'Topic :: Communications', 'Topic :: Internet', 17 | 'Topic :: Software Development :: Libraries', 18 | 'Topic :: Database'] 19 | 20 | requirements = ['tornado>4.0,<5', 'hiredis>=0.2.0,<1'] 21 | tests_require = ['nose', 'mock', 'codecov', 'coverage'] 22 | 23 | setuptools.setup(name='tredis', 24 | version='0.8.0', 25 | description='An asynchronous Redis client for Tornado', 26 | long_description=open('README.rst').read(), 27 | author='Gavin M. Roy', 28 | author_email='gavinmroy@gmail.com', 29 | url='http://github.com/gmr/tredis', 30 | packages=['tredis'], 31 | package_data={'': ['LICENSE', 'README.rst']}, 32 | include_package_data=True, 33 | install_requires=requirements, 34 | tests_require=tests_require, 35 | license='BSD', 36 | classifiers=classifiers) 37 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | codecov 3 | nose 4 | mock 5 | yapf 6 | pep8 7 | -r requirements.txt 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | 5 | def setup_module(): 6 | try: 7 | with open('build/test-environment') as env_file: 8 | for line_num, line in enumerate(env_file): 9 | if line.startswith('export '): 10 | line = line[7:].strip() 11 | name, sep, value = line.partition('=') 12 | name, value = name.strip(), value.strip() 13 | if sep != '=': 14 | logging.warning( 15 | 'ignoring line %d in environment file - %s', 16 | line_num, line) 17 | elif value: 18 | logging.debug('setting environment variable %s = %r', 19 | name, value) 20 | os.environ[name] = value 21 | else: 22 | logging.debug('clearing environment variable %s', name) 23 | os.environ.pop('name', None) 24 | 25 | except IOError: 26 | logging.info('expected environment file at build/env-vars') 27 | 28 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import logging 4 | import socket 5 | import time 6 | import uuid 7 | 8 | import mock 9 | from tornado import concurrent, testing 10 | 11 | import tredis 12 | from tredis import common 13 | 14 | # os.environ['ASYNC_TEST_TIMEOUT'] = '10' 15 | 16 | 17 | def split_connection_host_port(value): 18 | logging.debug('Returning alternate host for %s', value) 19 | parts = value.split(':') 20 | return os.environ['REDIS_HOST'], int(parts[1]) 21 | 22 | common.split_connection_host_port = split_connection_host_port 23 | 24 | 25 | class AsyncTestCase(testing.AsyncTestCase): 26 | 27 | AUTO_CONNECT = True 28 | CLUSTERING = False 29 | DEFAULT_EXPIRATION = 5 30 | 31 | def setUp(self): 32 | super(AsyncTestCase, self).setUp() 33 | self.client = self.get_client() 34 | self._execute_result = None 35 | 36 | def get_client(self): 37 | return tredis.Client( 38 | [{'host': self.redis_host, 39 | 'port': self.redis_port, 40 | 'db': self.redis_db}], 41 | clustering=self.CLUSTERING, 42 | auto_connect=self.AUTO_CONNECT) 43 | 44 | def tearDown(self): 45 | try: 46 | self.client.close() 47 | except tredis.ConnectionError: 48 | pass 49 | 50 | def reset_slave_relationship(self): 51 | logging.debug('Resetting slave relationship') 52 | self.disable_slave() 53 | time.sleep(0.5) 54 | self.enable_slave() 55 | time.sleep(0.5) 56 | 57 | @staticmethod 58 | def disable_slave(): 59 | logging.debug('Disabling slave mode on node2') 60 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 61 | socket.IPPROTO_TCP) 62 | with contextlib.closing(s): 63 | sockaddr = (os.environ['REDIS_HOST'], 64 | int(os.environ['REDIS2_PORT'])) 65 | logging.debug('Making %r a slave of no one', sockaddr) 66 | s.connect(sockaddr) 67 | s.send('SLAVEOF NO ONE\r\n'.encode('ASCII')) 68 | 69 | @staticmethod 70 | def enable_slave(): 71 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 72 | socket.IPPROTO_TCP) 73 | with contextlib.closing(s): 74 | sockaddr = (os.environ['REDIS_HOST'], int(os.environ['REDIS2_PORT'])) 75 | logging.debug('making %r a slave of REDIS1', sockaddr) 76 | s.connect(sockaddr) 77 | s.send('SLAVEOF {0} {1}\r\n'.format( 78 | os.environ['REDIS_HOST'], 79 | os.environ['REDIS1_PORT']).encode('ASCII')) 80 | 81 | @property 82 | def redis_host(self): 83 | return os.environ.get('REDIS_HOST', '127.0.0.1') 84 | 85 | @property 86 | def redis_port(self): 87 | return int(os.environ.get('REDIS1_PORT', '6379')) 88 | 89 | @property 90 | def redis_db(self): 91 | return int(os.environ.get('REDIS_DB', '12')) 92 | 93 | def expiring_set(self, key, value, expiration=None, nx=None, xx=None): 94 | return self.client.set(key, value, 95 | expiration or self.DEFAULT_EXPIRATION, 96 | nx=nx, xx=xx) 97 | 98 | @staticmethod 99 | def uuid4(qty=1): 100 | if qty == 1: 101 | return str(uuid.uuid4()).encode('ascii') 102 | else: 103 | return tuple([str(uuid.uuid4()).encode('ascii') 104 | for i in range(0, qty)]) 105 | 106 | def _execute(self, parts, expectation=None, format_callback=None): 107 | future = concurrent.Future() 108 | if isinstance(self._execute_result, Exception): 109 | future.set_exception(self._execute_result) 110 | else: 111 | future.set_result(self._execute_result) 112 | return future 113 | -------------------------------------------------------------------------------- /tests/cluster_behavior_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pprint 3 | import uuid 4 | 5 | from tornado import testing 6 | 7 | import tredis 8 | 9 | from . import base 10 | 11 | os.environ['ASYNC_TEST_TIMEOUT'] = '10' 12 | 13 | 14 | class AsyncTestCase(base.AsyncTestCase): 15 | 16 | AUTO_CONNECT = False 17 | CLUSTERING = True 18 | 19 | @property 20 | def redis_port(self): 21 | return int(os.environ['NODE1_PORT']) 22 | 23 | 24 | class ClusterBehaviorTests(AsyncTestCase): 25 | 26 | @testing.gen_test 27 | def test_connection_move(self): 28 | yield self.client.connect() 29 | try: 30 | for offset in range(0, 20): 31 | key = str(uuid.uuid4()) 32 | yield self.client.set(key, b'1', ex=10) 33 | value = yield self.client.get(key) 34 | self.assertEqual(value, b'1') 35 | except tredis.RedisError as error: 36 | info = yield self.client.cluster_info() 37 | pprint.pprint(info) 38 | nodes = yield self.client.cluster_nodes() 39 | pprint.pprint(nodes) 40 | raise error 41 | 42 | -------------------------------------------------------------------------------- /tests/cluster_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tornado import gen, testing 4 | 5 | from . import base 6 | 7 | os.environ['ASYNC_TEST_TIMEOUT'] = '10' 8 | 9 | 10 | class ClusterTests(base.AsyncTestCase): 11 | 12 | CLUSTERING = True 13 | 14 | @property 15 | def redis_port(self): 16 | return int(os.environ['NODE1_PORT']) 17 | 18 | @testing.gen_test() 19 | def test_cluster_nodes_against_cluster(self): 20 | while not self.client.ready: 21 | yield gen.sleep(0.01) 22 | expectation = sorted([ 23 | (os.environ['REDIS_HOST'], int(os.environ['NODE1_PORT'])), 24 | (os.environ['REDIS_HOST'], int(os.environ['NODE2_PORT'])), 25 | (os.environ['REDIS_HOST'], int(os.environ['NODE3_PORT'])), 26 | ]) 27 | results = yield self.client.cluster_nodes() 28 | values = [] 29 | for node in results: 30 | values.append((node.ip, node.port)) 31 | self.assertListEqual(sorted(values), expectation) 32 | -------------------------------------------------------------------------------- /tests/connect_tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import mock 5 | import uuid 6 | 7 | from tornado import testing 8 | from tornado import gen 9 | 10 | import tredis 11 | from tredis import exceptions 12 | 13 | from . import base 14 | 15 | ADDR_PATTERN = re.compile(r'(addr=([\.\d:]+))') 16 | 17 | class BadConnectTestCase(base.AsyncTestCase): 18 | 19 | AUTO_CONNECT = False 20 | 21 | @property 22 | def redis_host(self): 23 | return '255.255.255.255' 24 | 25 | @testing.gen_test 26 | def test_bad_connect_raises_exception(self): 27 | with self.assertRaises(exceptions.ConnectError): 28 | yield self.client.connect() 29 | 30 | 31 | class BadConnectDBTestCase(base.AsyncTestCase): 32 | 33 | AUTO_CONNECT = False 34 | 35 | @property 36 | def redis_db(self): 37 | return 255 38 | 39 | @testing.gen_test 40 | def test_bad_connect_db_raises_exception(self): 41 | with self.assertRaises(exceptions.RedisError): 42 | yield self.client.connect() 43 | 44 | 45 | class ConnectTestCase(base.AsyncTestCase): 46 | 47 | @gen.coroutine 48 | def _kill_client(self, client): 49 | results = yield client._execute([b'CLIENT', b'LIST']) 50 | matches = ADDR_PATTERN.findall(results.decode('ascii')) 51 | value = None 52 | for match, addr in matches: 53 | value = addr 54 | self.assertIsNotNone(value, 'Could not find client') 55 | result = yield client._execute( 56 | [b'CLIENT', b'KILL', value.encode('ascii')]) 57 | logging.info('CLIENT KILL result: %r', result) 58 | 59 | @testing.gen_test 60 | def test_close_invokes_iostream_close(self): 61 | yield self.client.set('foo', 'bar', 1) # Establish the connection 62 | stream = self.client._connection._stream 63 | with mock.patch.object(stream, 'close') as close: 64 | self.client.close() 65 | close.assert_called_once_with() 66 | 67 | @testing.gen_test 68 | def test_on_close_callback_invoked(self): 69 | on_close = mock.Mock() 70 | client = tredis.RedisClient(os.getenv('REDIS_HOST', 'localhost'), 71 | int(os.getenv('REDIS1_PORT', '6379')), 0, 72 | on_close, 73 | auto_connect=False) 74 | yield client.connect() 75 | result = yield client.set('foo', 'bar', 10) 76 | self.assertTrue(result) 77 | yield self._kill_client(client) 78 | on_close.assert_called_once_with() 79 | 80 | @testing.gen_test 81 | def test_competing_connections(self): 82 | result1 = self.client.set('foo', 'bar', 10) 83 | result2 = self.client.set('foo', 'baz', 10) 84 | yield result1 85 | yield result2 86 | 87 | self.assertTrue(result1) 88 | self.assertTrue(result2) 89 | 90 | @testing.gen_test 91 | def test_competing_connections(self): 92 | result1 = self.client.set('foo', 'bar', 10) 93 | result2 = self.client.set('foo', 'baz', 10) 94 | yield result1 95 | yield result2 96 | self.assertTrue(result1) 97 | self.assertTrue(result2) 98 | 99 | @testing.gen_test 100 | def test_close_unopened_client(self): 101 | with self.assertRaises(exceptions.ConnectionError): 102 | self.client.close() 103 | -------------------------------------------------------------------------------- /tests/crc16_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from tredis import crc16 5 | 6 | 7 | class CRC16TestCase(unittest.TestCase): 8 | 9 | def test_for_expected_values(self): 10 | for offset, (value, expectation) in enumerate([ 11 | (b'123456789', 0x31c3), 12 | (b'Tornado is a Python web framework and asynchronous ' 13 | b'networking library, originally developed at FriendFeed.', 14 | 0x5a2a), 15 | (b'\xe2\x9c\x88', 0x8357)]): 16 | result = crc16.crc16(value) 17 | self.assertEqual( 18 | result, expectation, 19 | 'Offset {} did not match (0x{:x} != 0x{:x})'.format( 20 | offset, result, expectation)) 21 | -------------------------------------------------------------------------------- /tests/failover_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tornado import testing 4 | 5 | from . import base 6 | 7 | 8 | class FailoverTests(base.AsyncTestCase): 9 | 10 | AUTO_CONNECT = False 11 | 12 | def setUp(self): 13 | super(FailoverTests, self).setUp() 14 | self.reset_slave_relationship() 15 | 16 | @property 17 | def redis_port(self): 18 | return int(os.environ['REDIS2_PORT']) 19 | 20 | @testing.gen_test 21 | def test_that_hset_writes_to_master(self): 22 | yield self.client.connect() 23 | expectation = (self.client._connection.host, 24 | self.client._connection.port) 25 | key, field, value = self.uuid4(3) 26 | result = yield self.client.hset(key, field, value) 27 | self.assertEqual(result, 1) 28 | redis_addr = (self.client._connection.host, 29 | self.client._connection.port) 30 | self.assertNotEqual(redis_addr, expectation) 31 | -------------------------------------------------------------------------------- /tests/hash_tests.py: -------------------------------------------------------------------------------- 1 | from tornado import testing 2 | 3 | from . import base 4 | 5 | 6 | class HashTests(base.AsyncTestCase): 7 | 8 | @testing.gen_test 9 | def test_hset(self): 10 | key, field, value = self.uuid4(3) 11 | result = yield self.client.hset(key, field, value) 12 | self.assertEqual(result, 1) 13 | 14 | @testing.gen_test 15 | def test_hset_return_value_for_overwrite(self): 16 | key, field, init_value, new_value = self.uuid4(4) 17 | result = yield self.client.hset(key, field, init_value) 18 | self.assertEqual(result, 1) 19 | 20 | result = yield self.client.hset(key, field, new_value) 21 | self.assertEqual(result, 0) 22 | 23 | @testing.gen_test 24 | def test_hget(self): 25 | key, field, value = self.uuid4(3) 26 | result = yield self.client.hset(key, field, value) 27 | self.assertEqual(result, 1) 28 | result = yield self.client.hget(key, field) 29 | self.assertEqual(result, value) 30 | 31 | @testing.gen_test 32 | def test_hset_overwrites(self): 33 | key, field, init_value, new_value = self.uuid4(4) 34 | yield self.client.hset(key, field, init_value) 35 | yield self.client.hset(key, field, new_value) 36 | value = yield self.client.hget(key, field) 37 | self.assertEqual(value, new_value) 38 | 39 | @testing.gen_test 40 | def test_hgetall_returns_dict(self): 41 | key = self.uuid4() 42 | field1, value1 = self.uuid4(2) 43 | yield self.client.hset(key, field1, value1) 44 | result = yield self.client.hgetall(key) 45 | self.assertEqual(result, {field1: value1}) 46 | 47 | field2, value2 = self.uuid4(2) 48 | yield self.client.hset(key, field2, value2) 49 | 50 | result = yield self.client.hgetall(key) 51 | self.assertEqual(result, {field1: value1, field2: value2}) 52 | 53 | @testing.gen_test 54 | def test_hgetall_on_non_hash(self): 55 | key = self.uuid4() 56 | result = yield self.client.hgetall(key) 57 | self.assertEqual(result, {}) 58 | 59 | @testing.gen_test 60 | def test_hmset(self): 61 | key = self.uuid4() 62 | field1, value1 = self.uuid4(2) 63 | field2, value2 = self.uuid4(2) 64 | field3, value3 = self.uuid4(2) 65 | result = yield self.client.hmset( 66 | key, {field1: value1, field2: value2, field3: value3}) 67 | self.assertTrue(result) 68 | 69 | result = yield self.client.hgetall(key) 70 | self.assertEqual(result, 71 | {field1: value1, field2: value2, field3: value3}) 72 | 73 | @testing.gen_test 74 | def test_hmset_of_empty_dict(self): 75 | key = self.uuid4() 76 | result = yield self.client.hmset(key, {}) 77 | self.assertFalse(result) 78 | 79 | @testing.gen_test 80 | def test_hmget(self): 81 | key = self.uuid4() 82 | field1, value1 = self.uuid4(2) 83 | field2, value2 = self.uuid4(2) 84 | field3, value3 = self.uuid4(2) 85 | yield self.client.hmset( 86 | key, {field1: value1, field2: value2, field3: value3}) 87 | 88 | result = yield self.client.hmget(key, field1, field3) 89 | self.assertEqual(result, {field1: value1, field3: value3}) 90 | 91 | @testing.gen_test 92 | def test_that_hmget_returns_none_values_for_nonexistent_key(self): 93 | key = self.uuid4() 94 | result = yield self.client.hmget(key, 'one', 'two', 'three') 95 | self.assertEqual(result, {'one': None, 'two': None, 'three': None}) 96 | 97 | @testing.gen_test 98 | def test_hdel(self): 99 | key = self.uuid4() 100 | field1, value1 = self.uuid4(2) 101 | field2, value2 = self.uuid4(2) 102 | field3, value3 = self.uuid4(2) 103 | yield self.client.hmset( 104 | key, {field1: value1, field2: value2, field3: value3}) 105 | 106 | result = yield self.client.hdel(key, field1) 107 | self.assertEqual(result, 1) 108 | 109 | result = yield self.client.hgetall(key) 110 | self.assertEqual(result, {field2: value2, field3: value3}) 111 | 112 | result = yield self.client.hdel(key, field2, field3) 113 | self.assertEqual(result, 2) 114 | 115 | result = yield self.client.hgetall(key) 116 | self.assertEqual(result, {}) 117 | 118 | result = yield self.client.hdel(key, 'nofield') 119 | self.assertEqual(result, 0) 120 | 121 | result = yield self.client.hdel(self.uuid4(), 'nofield') 122 | self.assertEqual(result, 0) 123 | 124 | @testing.gen_test 125 | def test_hdel_without_fields_returns_zero(self): 126 | result = yield self.client.hdel(self.uuid4()) 127 | self.assertEqual(result, 0) 128 | 129 | @testing.gen_test 130 | def test_hexist(self): 131 | key = self.uuid4() 132 | result = yield self.client.hexists(key, 'nofield') 133 | self.assertFalse(result) 134 | 135 | yield self.client.hset(key, 'field', self.uuid4()) 136 | result = yield self.client.hexists(key, 'field') 137 | self.assertTrue(result) 138 | 139 | result = yield self.client.hexists(key, 'nofield') 140 | self.assertFalse(result) 141 | 142 | @testing.gen_test 143 | def test_hincrby(self): 144 | key, field = self.uuid4(2) 145 | 146 | result = yield self.client.hincrby(key, field, 1) 147 | self.assertEqual(result, 1) 148 | 149 | result = yield self.client.hget(key, field) 150 | self.assertEqual(result, b'1') 151 | 152 | result = yield self.client.hincrby(key, field, 0x7fffFFFFffffFFFE) 153 | self.assertEqual(result, 0x7fffFFFFffffFFFF) 154 | 155 | result = yield self.client.hincrby(key, field, -0x7fffFFFFffffFFFF) 156 | self.assertEqual(result, 0) 157 | 158 | @testing.gen_test 159 | def test_hincrbyfloat(self): 160 | key, field = self.uuid4(2) 161 | 162 | result = yield self.client.hincrbyfloat(key, field, 10.5) 163 | self.assertAlmostEqual(result, 10.500000) 164 | 165 | result = yield self.client.hincrbyfloat(key, field, 0.1) 166 | self.assertAlmostEqual(result, 10.600000) 167 | 168 | result = yield self.client.hincrbyfloat(key, field, -5) 169 | self.assertAlmostEqual(result, 5.600000) 170 | 171 | result = yield self.client.hincrbyfloat(key, field, 2.0e2) 172 | self.assertAlmostEqual(result, 205.600000) 173 | 174 | @testing.gen_test 175 | def test_hkeys(self): 176 | key, field1, field2, field3 = self.uuid4(4) 177 | 178 | result = yield self.client.hkeys(key) 179 | self.assertEqual(result, []) 180 | 181 | yield self.client.hmset(key, {field1: self.uuid4(), 182 | field2: self.uuid4(), 183 | field3: self.uuid4()}) 184 | result = yield self.client.hkeys(key) 185 | self.assertEqual(sorted(result), sorted([field1, field2, field3])) 186 | 187 | @testing.gen_test 188 | def test_hlen(self): 189 | key = self.uuid4() 190 | 191 | result = yield self.client.hlen(key) 192 | self.assertEqual(result, 0) 193 | 194 | yield self.client.hmset(key, {'a': 1, 'b': 2, 'c': 3}) 195 | result = yield self.client.hlen(key) 196 | self.assertEqual(result, 3) 197 | 198 | @testing.gen_test 199 | def test_hsetnx(self): 200 | key, field, value = self.uuid4(3) 201 | 202 | result = yield self.client.hsetnx(key, field, value) 203 | self.assertEqual(result, 1) 204 | 205 | result = yield self.client.hget(key, field) 206 | self.assertEqual(result, value) 207 | 208 | result = yield self.client.hsetnx(key, field, self.uuid4()) 209 | self.assertEqual(result, 0) 210 | 211 | result = yield self.client.hget(key, field) 212 | self.assertEqual(result, value) 213 | 214 | @testing.gen_test 215 | def test_hvals(self): 216 | key = self.uuid4() 217 | result = yield self.client.hvals(key) 218 | self.assertEqual(result, []) 219 | 220 | yield self.client.hmset(key, 221 | {'f1': 'HelloWorld', 'f2': 99, 'f3': -256}) 222 | 223 | result = yield self.client.hvals(key) 224 | self.assertEqual(sorted(result), 225 | sorted([b'HelloWorld', b'99', b'-256'])) 226 | -------------------------------------------------------------------------------- /tests/hyperloglog_tests.py: -------------------------------------------------------------------------------- 1 | from tornado import testing 2 | 3 | from . import base 4 | 5 | 6 | class HyperLogLogTests(base.AsyncTestCase): 7 | 8 | @testing.gen_test 9 | def test_hyperloglog(self): 10 | key1, key2, key3 = self.uuid4(3) 11 | result = yield self.client.pfadd(key1, 'foo', 'bar', 'zap', 'a') 12 | self.assertTrue(result) 13 | result = yield self.client.pfadd(key2, 'a', 'b', 'c', 'foo') 14 | self.assertTrue(result) 15 | result = yield self.client.pfmerge(key3, key1, key2) 16 | self.assertTrue(result) 17 | result = yield self.client.pfcount(key3) 18 | self.assertEqual(result, 6) 19 | result = yield self.client.delete(key3, key1, key2) 20 | self.assertTrue(result) 21 | -------------------------------------------------------------------------------- /tests/keys_tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import mock 3 | import os 4 | import time 5 | import uuid 6 | 7 | from tornado import gen, testing 8 | 9 | import tredis 10 | from tredis import exceptions 11 | 12 | from . import base 13 | 14 | 15 | class KeyCommandTests(base.AsyncTestCase): 16 | 17 | @testing.gen_test 18 | def test_delete(self): 19 | key, value = self.uuid4(2) 20 | result = yield self.expiring_set(key, value) 21 | self.assertTrue(result) 22 | result = yield self.client.delete(key) 23 | self.assertTrue(result) 24 | 25 | @testing.gen_test 26 | def test_delete_multi(self): 27 | key1, key2, value = self.uuid4(3) 28 | result = yield self.expiring_set(key1, value) 29 | self.assertTrue(result) 30 | result = yield self.expiring_set(key2, value) 31 | self.assertTrue(result) 32 | result = yield self.client.delete(key1, key2) 33 | self.assertTrue(result) 34 | 35 | @testing.gen_test 36 | def test_delete_missing_key(self): 37 | key1, key2, value = self.uuid4(3) 38 | result = yield self.expiring_set(key1, value) 39 | self.assertTrue(result) 40 | result = yield self.expiring_set(key2, value) 41 | self.assertTrue(result) 42 | result = yield self.client.delete(key1, key2, 43 | self.uuid4()) 44 | self.assertEqual(result, 2) 45 | 46 | @testing.gen_test 47 | def test_dump_and_restore(self): 48 | key, value = self.uuid4(2) 49 | result = yield self.expiring_set(key, value) 50 | self.assertTrue(result) 51 | dump_value = yield self.client.dump(key) 52 | self.assertIn(value, dump_value) 53 | result = yield self.client.delete(key) 54 | self.assertTrue(result) 55 | result = yield self.client.restore(key, 10, dump_value) 56 | self.assertTrue(result) 57 | result = yield self.client.get(key) 58 | self.assertEqual(result, value) 59 | 60 | @testing.gen_test 61 | def test_dump_and_restore_with_replace(self): 62 | key, value1, value2 = self.uuid4(3) 63 | result = yield self.expiring_set(key, value1) 64 | self.assertTrue(result) 65 | dump_value = yield self.client.dump(key) 66 | self.assertIn(value1, dump_value) 67 | result = yield self.client.delete(key) 68 | self.assertTrue(result) 69 | result = yield self.expiring_set(key, value2) 70 | self.assertTrue(result) 71 | result = yield self.client.get(key) 72 | self.assertEqual(result, value2) 73 | result = yield self.client.restore(key, 10, dump_value, True) 74 | self.assertTrue(result) 75 | result = yield self.client.get(key) 76 | self.assertEqual(result, value1) 77 | 78 | @testing.gen_test 79 | def test_dump_and_restore_without_replace(self): 80 | key, value1, value2 = self.uuid4(3) 81 | result = yield self.expiring_set(key, value1) 82 | self.assertTrue(result) 83 | dump_value = yield self.client.dump(key) 84 | self.assertIn(value1, dump_value) 85 | result = yield self.client.delete(key) 86 | self.assertTrue(result) 87 | result = yield self.expiring_set(key, value2) 88 | self.assertTrue(result) 89 | result = yield self.client.get(key) 90 | self.assertEqual(result, value2) 91 | with self.assertRaises(exceptions.RedisError): 92 | yield self.client.restore(key, 10, dump_value) 93 | 94 | @testing.gen_test 95 | def test_dump_with_invalid_key(self): 96 | key = self.uuid4() 97 | result = yield self.client.dump(key) 98 | self.assertIsNone(result) 99 | 100 | @testing.gen_test 101 | def test_expire_and_ttl(self): 102 | key, value = self.uuid4(2) 103 | ttl = 5 104 | result = yield self.expiring_set(key, value) 105 | self.assertTrue(result) 106 | result = yield self.client.expire(key, ttl) 107 | self.assertTrue(result) 108 | result = yield self.client.ttl(key) 109 | self.assertAlmostEqual(result, ttl) 110 | 111 | @testing.gen_test 112 | def test_expire_with_error(self): 113 | with self.assertRaises(exceptions.RedisError): 114 | yield self.client.expire(self.uuid4(), 'abc') 115 | 116 | @testing.gen_test 117 | def test_expireat_and_ttl(self): 118 | key, value = self.uuid4(2) 119 | result = yield self.expiring_set(key, value) 120 | self.assertTrue(result) 121 | 122 | timestamp = (yield self.client.time()) + 5.0 123 | result = yield self.client.expireat(key, int(timestamp)) 124 | self.assertTrue(result) 125 | result = yield self.client.ttl(key) 126 | self.assertLessEqual(result, 5) 127 | self.assertGreater(result, 0) 128 | 129 | @testing.gen_test 130 | def test_expireat_with_error(self): 131 | with self.assertRaises(exceptions.RedisError): 132 | yield self.client.expireat(self.uuid4(), 'abc') 133 | 134 | @testing.gen_test 135 | def test_exists_single(self): 136 | key, value = self.uuid4(2) 137 | result = yield self.expiring_set(key, value) 138 | self.assertTrue(result) 139 | result = yield self.client.exists(key) 140 | self.assertTrue(result) 141 | 142 | @testing.gen_test 143 | def test_exists_none(self): 144 | key = self.uuid4() 145 | result = yield self.client.exists(key) 146 | self.assertFalse(result) 147 | 148 | @testing.gen_test 149 | def test_keys(self): 150 | yield self.client.select(3) 151 | prefix = self.uuid4() 152 | keys = ['{}-{}'.format(prefix, str(uuid.uuid4())).encode('ascii') 153 | for i in range(0, 10)] 154 | for key in keys: 155 | yield self.expiring_set(key, str(uuid.uuid4())) 156 | result = yield self.client.keys('{}-*'.format(prefix)) 157 | self.assertListEqual(sorted(result), sorted(keys)) 158 | 159 | @testing.gen_test 160 | def test_move(self): 161 | response = yield self.client.select(2) 162 | self.assertTrue(response) 163 | 164 | key, value = self.uuid4(2) 165 | response = yield self.expiring_set(key, value) 166 | self.assertTrue(response) 167 | response = yield self.client.move(key, 1) 168 | self.assertTrue(response) 169 | response = yield self.client.select(1) 170 | self.assertTrue(response) 171 | response = yield self.client.get(key) 172 | self.assertEqual(response, value) 173 | 174 | @testing.gen_test 175 | def test_object_encoding(self): 176 | key, value1, value2 = self.uuid4(3) 177 | result = yield self.client.sadd(key, value1, value2) 178 | self.assertTrue(result) 179 | result = yield self.client.object_encoding(key) 180 | self.assertEqual(result, b'hashtable') 181 | result = yield self.client.delete(key) 182 | self.assertTrue(result) 183 | 184 | @testing.gen_test 185 | def test_object_idle_time(self): 186 | key, value1, value2 = self.uuid4(3) 187 | result = yield self.client.sadd(key, value1, value2) 188 | self.assertTrue(result) 189 | result = yield self.client.object_idle_time(key) 190 | self.assertEqual(result, 0) 191 | result = yield self.client.delete(key) 192 | self.assertTrue(result) 193 | 194 | @testing.gen_test 195 | def test_object_refcount(self): 196 | key = self.uuid4() 197 | for value in self.uuid4(3): 198 | result = yield self.client._execute([b'ZADD', key, b'1', value]) 199 | self.assertTrue(result) 200 | result = yield self.client.object_refcount(key) 201 | self.assertEqual(result, 1) 202 | result = yield self.client.delete(key) 203 | self.assertTrue(result) 204 | 205 | @testing.gen_test 206 | def test_persist(self): 207 | key, value = self.uuid4(2) 208 | result = yield self.expiring_set(key, value) 209 | self.assertTrue(result) 210 | result = yield self.client.expire(key, 10) 211 | self.assertTrue(result) 212 | result = yield self.client.persist(key) 213 | self.assertTrue(result) 214 | result = yield self.client.ttl(key) 215 | self.assertEqual(result, -1) 216 | 217 | @testing.gen_test 218 | def test_pexpire_and_ttl(self): 219 | key, value = self.uuid4(2) 220 | result = yield self.expiring_set(key, value) 221 | self.assertTrue(result) 222 | result = yield self.client.pexpire(key, 5000) 223 | self.assertTrue(result) 224 | result = yield self.client.ttl(key) 225 | self.assertAlmostEqual(result, 5) 226 | 227 | @testing.gen_test 228 | def test_pexpire_with_error(self): 229 | with self.assertRaises(exceptions.RedisError): 230 | yield self.client.pexpire(self.uuid4(), 'abc') 231 | 232 | @testing.gen_test 233 | def test_pexpireat_and_ttl(self): 234 | key, value = self.uuid4(2) 235 | result = yield self.expiring_set(key, value) 236 | self.assertTrue(result) 237 | 238 | timestamp = (yield self.client.time()) + 5.0 239 | result = yield self.client.pexpireat(key, int(timestamp * 1000.0)) 240 | self.assertTrue(result) 241 | result = yield self.client.ttl(key) 242 | 243 | self.assertLessEqual(result, 5) 244 | self.assertGreater(result, 0) 245 | 246 | @testing.gen_test 247 | def test_pexpireat_with_error(self): 248 | with self.assertRaises(exceptions.RedisError): 249 | yield self.client.pexpireat(self.uuid4(), 'abc') 250 | 251 | @testing.gen_test 252 | def test_pttl(self): 253 | key, value = self.uuid4(2) 254 | result = yield self.expiring_set(key, value) 255 | self.assertTrue(result) 256 | result = yield self.client.pexpire(key, 5000) 257 | self.assertTrue(result) 258 | result = yield self.client.pttl(key) 259 | self.assertGreater(result, 1000) 260 | self.assertLessEqual(result, 5000) 261 | 262 | @testing.gen_test 263 | def test_randomkey(self): 264 | yield self.client.set('foo', 'bar', ex=1) 265 | yield self.client.select(4) 266 | keys = yield self.client.keys('*') 267 | self.client.delete(*keys) 268 | 269 | keys = self.uuid4(10) 270 | for key in list(keys): 271 | yield self.expiring_set(key, str(uuid.uuid4())) 272 | result = yield self.client.randomkey() 273 | self.assertIn(result, keys) 274 | 275 | @testing.gen_test 276 | def test_rename(self): 277 | key1, key2, value = self.uuid4(3) 278 | result = yield self.expiring_set(key1, value) 279 | self.assertTrue(result) 280 | result = yield self.client.rename(key1, key2) 281 | self.assertTrue(result) 282 | result = yield self.client.get(key2) 283 | self.assertEqual(result, value) 284 | 285 | @testing.gen_test 286 | def test_renamenx(self): 287 | key1, key2, value = self.uuid4(3) 288 | result = yield self.expiring_set(key1, value) 289 | self.assertTrue(result) 290 | 291 | result = yield self.client.get(key2) 292 | self.assertIsNone(result) 293 | 294 | result = yield self.client.renamenx(key1, key2) 295 | self.assertTrue(result) 296 | result = yield self.client.get(key2) 297 | self.assertEqual(result, value) 298 | 299 | @testing.gen_test 300 | def test_renamenx_failure(self): 301 | key1, key2, value = self.uuid4(3) 302 | result = yield self.expiring_set(key1, value) 303 | self.assertTrue(result) 304 | result = yield self.expiring_set(key2, value) 305 | self.assertTrue(result) 306 | result = yield self.client.renamenx(key1, key2) 307 | self.assertFalse(result) 308 | 309 | @testing.gen_test 310 | def test_scan(self): 311 | yield self.client.select(5) 312 | key1, key2, key3, value = self.uuid4(4) 313 | keys = [key1, key2, key3] 314 | for key in keys: 315 | result = yield self.expiring_set(key, value) 316 | self.assertTrue(result) 317 | cursor, result = yield self.client.scan(0) 318 | self.assertListEqual(sorted(result), sorted(keys)) 319 | self.assertEqual(cursor, 0) 320 | for key in keys: 321 | result = yield self.client.delete(key) 322 | self.assertTrue(result) 323 | 324 | @testing.gen_test 325 | def test_scan_with_pattern(self): 326 | yield self.client.select(5) 327 | key1, key2, key3, value = self.uuid4(4) 328 | keys = [key1, key2, key3] 329 | for key in keys: 330 | result = yield self.expiring_set(key, value) 331 | self.assertTrue(result) 332 | cursor, result = yield self.client.scan(0, '*') 333 | self.assertListEqual(sorted(result), sorted(keys)) 334 | self.assertEqual(cursor, 0) 335 | for key in keys: 336 | result = yield self.client.delete(key) 337 | self.assertTrue(result) 338 | 339 | @testing.gen_test 340 | def test_scan_with_pattern_and_count(self): 341 | yield self.client.select(5) 342 | key1, key2, key3, value = self.uuid4(4) 343 | keys = [key1, key2, key3] 344 | for key in keys: 345 | result = yield self.expiring_set(key, value) 346 | self.assertTrue(result) 347 | cursor, result = yield self.client.scan(0, '*', 10) 348 | self.assertListEqual(sorted(result), sorted(keys)) 349 | self.assertEqual(cursor, 0) 350 | for key in keys: 351 | result = yield self.client.delete(key) 352 | self.assertTrue(result) 353 | 354 | @testing.gen_test 355 | def test_scan_with_error(self): 356 | key = self.uuid4() 357 | self._execute_result = exceptions.RedisError('Test Exception') 358 | with mock.patch.object(self.client, '_execute', self._execute): 359 | with self.assertRaises(exceptions.RedisError): 360 | yield self.client.scan(key, 0) 361 | 362 | @testing.gen_test 363 | def test_sort_invalid_order(self): 364 | key = self.uuid4() 365 | with self.assertRaises(ValueError): 366 | yield self.client.sort(key, alpha=True, order='DOWN') 367 | 368 | @testing.gen_test 369 | def test_sort_numeric(self): 370 | key = self.uuid4() 371 | result = yield self.client.sadd(key, 100, 300, 200) 372 | self.assertTrue(result) 373 | result = yield self.client.sort(key) 374 | self.assertListEqual(result, [b'100', b'200', b'300']) 375 | result = yield self.client.delete(key) 376 | self.assertTrue(result) 377 | 378 | @testing.gen_test 379 | def test_sort_alpha_asc(self): 380 | key, value1, value2, value3 = self.uuid4(4) 381 | result = yield self.client.sadd(key, value1, value2, value3) 382 | self.assertTrue(result) 383 | result = yield self.client.sort(key, alpha=True) 384 | self.assertListEqual(result, sorted([value1, value2, value3])) 385 | result = yield self.client.delete(key) 386 | self.assertTrue(result) 387 | 388 | @testing.gen_test 389 | def test_sort_alpha_desc(self): 390 | key, value1, value2, value3 = self.uuid4(4) 391 | result = yield self.client.sadd(key, value1, value2, value3) 392 | self.assertTrue(result) 393 | result = yield self.client.sort(key, alpha=True, order='DESC') 394 | self.assertListEqual(result, sorted([value1, value2, value3], 395 | reverse=True)) 396 | result = yield self.client.delete(key) 397 | self.assertTrue(result) 398 | 399 | @testing.gen_test 400 | def test_sort_alpha_limit_offset(self): 401 | key, value1, value2, value3 = self.uuid4(4) 402 | result = yield self.client.sadd(key, value1, value2, value3) 403 | self.assertTrue(result) 404 | result = yield self.client.sort(key, limit=2, offset=1, alpha=True) 405 | self.assertListEqual(result, sorted([value1, value2, value3])[1:]) 406 | result = yield self.client.delete(key) 407 | self.assertTrue(result) 408 | 409 | @testing.gen_test 410 | def test_sort_alpha_asc_and_store(self): 411 | key1, key2, value1, value2, value3 = self.uuid4(5) 412 | result = yield self.client.sadd(key1, value1, value2, value3) 413 | self.assertTrue(result) 414 | result = yield self.client.sort(key1, alpha=True, store_as=key2) 415 | self.assertEqual(result, 3) 416 | result = yield self.client.type(key2) 417 | result = yield self.client._execute([b'LRANGE', key2, 0, 3]) 418 | self.assertListEqual(result, sorted([value1, value2, value3])) 419 | result = yield self.client.delete(key1) 420 | self.assertTrue(result) 421 | result = yield self.client.delete(key2) 422 | self.assertTrue(result) 423 | 424 | @testing.gen_test 425 | def test_sort_by(self): 426 | key, value1, value2, value3 = self.uuid4(4) 427 | values = [value1, value2, value3] 428 | result = yield self.client.sadd(key, value1, value2, value3) 429 | self.assertTrue(result) 430 | expectation = [] 431 | for index, value in enumerate(values): 432 | weight_key = 'weight1_{}'.format(value.decode('utf-8')) 433 | result = yield self.expiring_set(weight_key, index) 434 | self.assertTrue(result) 435 | expectation.append(value) 436 | 437 | result = yield self.client.sort(key, by='weight1_*') 438 | self.assertListEqual(result, expectation) 439 | result = yield self.client.delete(key) 440 | self.assertTrue(result) 441 | 442 | @testing.gen_test 443 | def test_sort_by_with_external(self): 444 | key, value1, value2, value3 = self.uuid4(4) 445 | values = [value1, value2, value3] 446 | result = yield self.client.sadd(key, value1, value2, value3) 447 | self.assertTrue(result) 448 | expectation = [] 449 | for index, value in enumerate(values): 450 | weight_key = 'weight2_{}'.format(value.decode('utf-8')) 451 | result = yield self.expiring_set(weight_key, index) 452 | self.assertTrue(result) 453 | 454 | ext_key = 'obj2_{}'.format(value.decode('utf-8')) 455 | ext_val = 'value: {}'.format(index).encode('utf-8') 456 | result = yield self.expiring_set(ext_key, ext_val) 457 | self.assertTrue(result) 458 | expectation.append(ext_val) 459 | 460 | result = yield self.client.sort(key, by='weight2_*', 461 | external='obj2_*') 462 | self.assertListEqual(result, expectation) 463 | result = yield self.client.delete(key) 464 | self.assertTrue(result) 465 | 466 | @testing.gen_test 467 | def test_sort_by_with_externals(self): 468 | key, value1, value2, value3 = self.uuid4(4) 469 | values = [value1, value2, value3] 470 | result = yield self.client.sadd(key, value1, value2, value3) 471 | self.assertTrue(result) 472 | expectation = [] 473 | for index, value in enumerate(values): 474 | weight_key = 'weight2_{}'.format(value.decode('utf-8')) 475 | result = yield self.expiring_set(weight_key, index) 476 | self.assertTrue(result) 477 | 478 | ext_key = 'obj2a_{}'.format(value.decode('utf-8')) 479 | ext_val = 'value1: {}'.format(index).encode('utf-8') 480 | result = yield self.expiring_set(ext_key, ext_val) 481 | self.assertTrue(result) 482 | expectation.append(ext_val) 483 | 484 | ext_key = 'obj2b_{}'.format(value.decode('utf-8')) 485 | ext_val = 'value2: {}'.format(index).encode('utf-8') 486 | result = yield self.expiring_set(ext_key, ext_val) 487 | self.assertTrue(result) 488 | expectation.append(ext_val) 489 | 490 | result = yield self.client.sort(key, by='weight2_*', 491 | external=['obj2a_*', 'obj2b_*']) 492 | self.assertListEqual(result, expectation) 493 | result = yield self.client.delete(key) 494 | self.assertTrue(result) 495 | 496 | @testing.gen_test 497 | def test_type_string(self): 498 | key, value = self.uuid4(2) 499 | result = yield self.expiring_set(key, value) 500 | self.assertTrue(result) 501 | result = yield self.client.type(key) 502 | self.assertEqual(result, b'string') 503 | 504 | @testing.gen_test 505 | def test_type_set(self): 506 | key, value = self.uuid4(2) 507 | result = yield self.client.sadd(key, value) 508 | self.assertTrue(result) 509 | result = yield self.client.type(key) 510 | self.assertEqual(result, b'set') 511 | result = yield self.client.delete(key) 512 | self.assertTrue(result) 513 | 514 | @testing.gen_test 515 | def test_wait(self): 516 | key, value = self.uuid4(2) 517 | result = yield self.expiring_set(key, value) 518 | self.assertTrue(result) 519 | result = yield self.client.wait(0, 500) 520 | self.assertEqual(result, 0) 521 | 522 | 523 | class MigrationTests(base.AsyncTestCase): 524 | 525 | def setUp(self): 526 | super(MigrationTests, self).setUp() 527 | self.redis2_ip = os.getenv('REDIS2_IP', 'localhost') 528 | self.redis2_port = int(os.getenv('REDIS2_PORT', 6379)) 529 | self.disable_slave() 530 | 531 | @testing.gen_test 532 | def test_migrate(self): 533 | key, value = self.uuid4(2) 534 | result = yield self.expiring_set(key, value) 535 | self.assertTrue(result) 536 | result = yield self.client.migrate(self.redis2_ip, 6379, key, 10, 537 | 5000) 538 | self.assertTrue(result) 539 | 540 | client = tredis.RedisClient(self.redis_host, self.redis2_port, 10, 541 | auto_connect=False) 542 | yield client.connect() 543 | 544 | response = yield client.get(key) 545 | self.assertEqual(response, value) 546 | result = yield self.client.get(key) 547 | self.assertIsNone(result) 548 | 549 | @testing.gen_test 550 | def test_migrate_copy(self): 551 | key, value = self.uuid4(2) 552 | result = yield self.expiring_set(key, value) 553 | self.assertTrue(result) 554 | result = yield self.client.migrate(self.redis2_ip, 6379, key, 10, 555 | 5000, copy=True) 556 | self.assertTrue(result) 557 | 558 | client = tredis.RedisClient(self.redis_host, self.redis2_port, 10, 559 | auto_connect=False) 560 | yield client.connect() 561 | 562 | result = yield client.get(key) 563 | self.assertEqual(result, value) 564 | result = yield self.client.get(key) 565 | self.assertEqual(result, value) 566 | 567 | @testing.gen_test 568 | def test_migrate_exists(self): 569 | key, value = self.uuid4(2) 570 | result = yield self.expiring_set(key, value) 571 | self.assertTrue(result) 572 | 573 | client = tredis.RedisClient(self.redis_host, self.redis2_port, 10, 574 | auto_connect=False) 575 | yield client.connect() 576 | 577 | result = yield client.set(key, value, 10) 578 | self.assertTrue(result) 579 | with self.assertRaises(exceptions.RedisError): 580 | yield self.client.migrate(self.redis2_ip, 6379, key, 10, 5000) 581 | 582 | @testing.gen_test 583 | def test_migrate_replace(self): 584 | key, value = self.uuid4(2) 585 | result = yield self.expiring_set(key, value) 586 | self.assertTrue(result) 587 | 588 | client = tredis.RedisClient(self.redis_host, self.redis2_port, 10, 589 | auto_connect=False) 590 | yield client.connect() 591 | 592 | result = yield client.set(key, value, 10) 593 | self.assertTrue(result) 594 | 595 | result = yield self.client.migrate(self.redis2_ip, 6379, 596 | key, 10, 5000, replace=True) 597 | self.assertTrue(result) 598 | 599 | result = yield client.get(key) 600 | self.assertEqual(result, value) 601 | 602 | result = yield self.client.get(key) 603 | self.assertIsNone(result) 604 | -------------------------------------------------------------------------------- /tests/list_tests.py: -------------------------------------------------------------------------------- 1 | from tornado import testing 2 | 3 | import tredis.exceptions 4 | 5 | from . import base 6 | 7 | 8 | class ListTests(base.AsyncTestCase): 9 | 10 | @testing.gen_test 11 | def test_llen(self): 12 | key = self.uuid4() 13 | values = self.uuid4(5) 14 | yield self.client.lpush(key, *values) 15 | result = yield self.client.llen(key) 16 | self.assertEqual(result, len(values)) 17 | 18 | @testing.gen_test 19 | def test_llen_of_nonexistent_list(self): 20 | result = yield self.client.llen(self.uuid4()) 21 | self.assertEqual(result, 0) 22 | 23 | @testing.gen_test 24 | def test_llen_of_nonlist(self): 25 | key = self.uuid4() 26 | yield self.client.sadd(key, self.uuid4()) 27 | with self.assertRaises(tredis.exceptions.TRedisException): 28 | yield self.client.llen(key) 29 | 30 | @testing.gen_test 31 | def test_lpush(self): 32 | key, value = self.uuid4(2) 33 | result = yield self.client.lpush(key, value) 34 | self.assertEqual(result, 1) # list length 35 | 36 | @testing.gen_test 37 | def test_lpush_then_lpop(self): 38 | key, value = self.uuid4(2) 39 | yield self.client.lpush(key, value) 40 | result = yield self.client.lpop(key) 41 | self.assertEqual(result, value) 42 | 43 | @testing.gen_test 44 | def test_lpop_of_nonexistent_list(self): 45 | result = yield self.client.lpop(self.uuid4()) 46 | self.assertIsNone(result) 47 | 48 | @testing.gen_test 49 | def test_lpushx_of_nonlist(self): 50 | key, name, value = self.uuid4(3) 51 | yield self.client.hset(key, name, value) 52 | with self.assertRaises(tredis.exceptions.RedisError): 53 | yield self.client.lpushx(key, self.uuid4()) 54 | 55 | @testing.gen_test 56 | def test_lpushx_of_nonexistent_list(self): 57 | key, value = self.uuid4(2) 58 | result = yield self.client.lpushx(key, value) 59 | self.assertEqual(result, 0) 60 | 61 | @testing.gen_test 62 | def test_ltrim(self): 63 | key = self.uuid4() 64 | result = yield self.client.ltrim(key, 0, 10) 65 | values = self.uuid4(10) 66 | yield self.client.lpush(key, *values) 67 | result = yield self.client.ltrim(key, 0, 4) 68 | self.assertIs(result, True) 69 | 70 | result = yield self.client.llen(key) 71 | self.assertEqual(result, 5) 72 | 73 | @testing.gen_test 74 | def test_rpush(self): 75 | key, value = self.uuid4(2) 76 | result = yield self.client.rpush(key, value) 77 | self.assertEqual(result, 1) # list length 78 | 79 | @testing.gen_test 80 | def test_rpushx_of_nonlist(self): 81 | key, name, value = self.uuid4(3) 82 | yield self.client.hset(key, name, value) 83 | with self.assertRaises(tredis.exceptions.RedisError): 84 | yield self.client.rpushx(key, self.uuid4()) 85 | 86 | @testing.gen_test 87 | def test_rpushx_of_nonexistent_list(self): 88 | key, value = self.uuid4(2) 89 | result = yield self.client.rpushx(key, value) 90 | self.assertEqual(result, 0) 91 | 92 | @testing.gen_test 93 | def test_rpush_then_rpop(self): 94 | key, value = self.uuid4(2) 95 | yield self.client.rpush(key, value) 96 | result = yield self.client.rpop(key) 97 | self.assertEqual(result, value) 98 | 99 | @testing.gen_test 100 | def test_lrange(self): 101 | key = self.uuid4() 102 | values = list(self.uuid4(10)) 103 | yield self.client.rpush(key, *values) 104 | 105 | result = yield self.client.lrange(key, 0, 4) 106 | self.assertEqual(result, values[:5]) 107 | 108 | result = yield self.client.lrange(key, 0, -1) 109 | self.assertEqual(result, values) 110 | 111 | result = yield self.client.lrange(key, 0, -(len(values) + 1)) 112 | self.assertEqual(result, []) 113 | 114 | result = yield self.client.lrange(key, 0, 10 * len(values)) 115 | self.assertEqual(result, values) 116 | -------------------------------------------------------------------------------- /tests/scripting_tests.py: -------------------------------------------------------------------------------- 1 | from tornado import testing 2 | 3 | from tredis import exceptions 4 | 5 | from . import base 6 | 7 | 8 | TEST_SCRIPT = """\ 9 | return redis.call("set", KEYS[1], ARGV[1]) 10 | """ 11 | 12 | 13 | class ScriptingTests(base.AsyncTestCase): 14 | 15 | @testing.gen_test 16 | def test_eval(self): 17 | key, value = self.uuid4(2) 18 | result = yield self.client.eval(TEST_SCRIPT, [key], [value]) 19 | self.assertTrue(result) 20 | result = yield self.client.get(key) 21 | self.assertEqual(value, result) 22 | 23 | @testing.gen_test 24 | def test_load_and_evalsha(self): 25 | sha1 = yield self.client.script_load(TEST_SCRIPT) 26 | key, value = self.uuid4(2) 27 | result = yield self.client.evalsha(sha1, [key], [value]) 28 | self.assertTrue(result) 29 | result = yield self.client.get(key) 30 | self.assertEqual(value, result) 31 | 32 | @testing.gen_test 33 | def test_load_exists_and_flush(self): 34 | sha1 = yield self.client.script_load(TEST_SCRIPT) 35 | result = yield self.client.script_exists(sha1) 36 | self.assertListEqual(result, [1]) 37 | result = yield self.client.script_flush() 38 | self.assertTrue(result) 39 | result = yield self.client.script_exists(sha1) 40 | self.assertListEqual(result, [0]) 41 | 42 | @testing.gen_test 43 | def test_kill(self): 44 | with self.assertRaises(exceptions.RedisError): 45 | result = yield self.client.script_kill() 46 | self.assertFalse(result) 47 | -------------------------------------------------------------------------------- /tests/server_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | import mock 5 | import time 6 | 7 | from tornado import testing 8 | 9 | from tredis import exceptions 10 | 11 | from . import base 12 | 13 | 14 | class ServerTests(base.AsyncTestCase): 15 | 16 | @testing.gen_test 17 | def test_auth_raises_redis_error(self): 18 | with self.assertRaises(exceptions.RedisError): 19 | yield self.client.auth('boom-goes-the-silver-nitrate') 20 | 21 | @testing.gen_test 22 | def test_auth_raises_auth_error(self): 23 | self._execute_result = exceptions.RedisError(b'invalid password') 24 | with mock.patch.object(self.client, '_execute', self._execute): 25 | with self.assertRaises(exceptions.AuthError): 26 | yield self.client.auth('boom-goes-the-silver-nitrate') 27 | 28 | @testing.gen_test 29 | def test_auth_returns_true(self): 30 | self._execute_result = b'OK' 31 | with mock.patch.object(self.client, '_execute', self._execute): 32 | result = yield self.client.auth('password') 33 | self.assertTrue(result) 34 | 35 | @testing.gen_test 36 | def test_echo_response(self): 37 | value = b'echo-test' 38 | result = yield self.client.echo(value) 39 | self.assertEqual(result, value) 40 | 41 | @testing.gen_test 42 | def test_info_response(self): 43 | result = yield self.client.info() 44 | self.assertTrue(isinstance(result, dict)) 45 | for key in ['tcp_port', 'role', 'redis_version', 'redis_mode']: 46 | self.assertIn(key, result) 47 | 48 | @testing.gen_test 49 | def test_info_server_response(self): 50 | result = yield self.client.info('server') 51 | self.assertTrue(isinstance(result, dict)) 52 | for key in ['tcp_port', 'redis_version', 'redis_mode']: 53 | self.assertIn(key, result) 54 | 55 | @testing.gen_test 56 | def test_ping_response(self): 57 | result = yield self.client.ping() 58 | self.assertEqual(result, b'PONG') 59 | 60 | @testing.gen_test 61 | def test_quit_response(self): 62 | result = yield self.client.quit() 63 | self.assertTrue(result) 64 | 65 | @testing.gen_test 66 | def test_select_response(self): 67 | result = yield self.client.select(1) 68 | self.assertTrue(result) 69 | 70 | @testing.gen_test 71 | def test_time(self): 72 | now = time.time() 73 | result = yield self.client.time() 74 | self.assertTrue(now - 10 < int(result), now + 10) 75 | -------------------------------------------------------------------------------- /tests/sets_tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from tornado import testing 4 | 5 | from tredis import exceptions 6 | 7 | from . import base 8 | 9 | 10 | class SetTests(base.AsyncTestCase): 11 | 12 | @testing.gen_test 13 | def test_sadd_single(self): 14 | key, value = self.uuid4(2) 15 | result = yield self.client.sadd(key, value) 16 | self.assertEqual(result, 1) 17 | 18 | @testing.gen_test 19 | def test_sadd_multiple(self): 20 | key, value1, value2, value3 = self.uuid4(4) 21 | result = yield self.client.sadd(key, value1, value2, value3) 22 | self.assertTrue(result) 23 | 24 | @testing.gen_test 25 | def test_sadd_multiple_dupe(self): 26 | key, value1, value2, value3 = self.uuid4(4) 27 | result = yield self.client.sadd(key, value1, value2, value3, value3) 28 | self.assertEqual(result, 3) 29 | 30 | @testing.gen_test 31 | def test_sadd_with_error(self): 32 | key, value = self.uuid4(2) 33 | self._execute_result = exceptions.RedisError('Test Exception') 34 | with mock.patch.object(self.client, '_execute', self._execute): 35 | with self.assertRaises(exceptions.RedisError): 36 | yield self.client.sadd(key, value) 37 | 38 | @testing.gen_test 39 | def test_sdiff(self): 40 | key1, key2, value1, value2, value3 = self.uuid4(5) 41 | result = yield self.client.sadd(key1, value1, value2) 42 | self.assertTrue(result) 43 | result = yield self.client.sadd(key2, value1, value3) 44 | self.assertTrue(result) 45 | result = yield self.client.sdiff(key1, key2) 46 | self.assertListEqual(result, [value2]) 47 | 48 | @testing.gen_test 49 | def test_sdiffstore(self): 50 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 51 | result = yield self.client.sadd(key1, value1, value2) 52 | self.assertTrue(result) 53 | result = yield self.client.sadd(key2, value1, value3) 54 | self.assertTrue(result) 55 | result = yield self.client.sdiffstore(key3, key1, key2) 56 | self.assertEqual(result, 1) 57 | result = yield self.client.sismember(key3, value2) 58 | self.assertTrue(result) 59 | 60 | @testing.gen_test 61 | def test_sinter(self): 62 | key1, key2, value1, value2, value3 = self.uuid4(5) 63 | result = yield self.client.sadd(key1, value1, value2) 64 | self.assertTrue(result) 65 | result = yield self.client.sadd(key2, value2, value3) 66 | self.assertTrue(result) 67 | result = yield self.client.sinter(key1, key2) 68 | self.assertListEqual(result, [value2]) 69 | 70 | @testing.gen_test 71 | def test_sinterstore(self): 72 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 73 | result = yield self.client.sadd(key1, value1, value2) 74 | self.assertTrue(result) 75 | result = yield self.client.sadd(key2, value2, value3) 76 | self.assertTrue(result) 77 | result = yield self.client.sinterstore(key3, key1, key2) 78 | self.assertEqual(result, 1) 79 | result = yield self.client.sismember(key3, value2) 80 | self.assertTrue(result) 81 | 82 | @testing.gen_test 83 | def test_sadd_sismember_true(self): 84 | key, value = self.uuid4(2) 85 | result = yield self.client.sadd(key, value) 86 | self.assertTrue(result) 87 | result = yield self.client.sismember(key, value) 88 | self.assertTrue(result) 89 | 90 | @testing.gen_test 91 | def test_sadd_sismember_false(self): 92 | key, value1, value2 = self.uuid4(3) 93 | result = yield self.client.sadd(key, value1) 94 | self.assertTrue(result) 95 | result = yield self.client.sismember(key, value2) 96 | self.assertFalse(result) 97 | 98 | @testing.gen_test 99 | def test_scard(self): 100 | key, value1, value2, value3 = self.uuid4(4) 101 | result = yield self.client.sadd(key, value1, value2, value3) 102 | self.assertTrue(result) 103 | result = yield self.client.scard(key) 104 | self.assertEqual(result, 3) 105 | 106 | @testing.gen_test 107 | def test_smembers(self): 108 | key, value1, value2, value3 = self.uuid4(4) 109 | result = yield self.client.sadd(key, value1, value2, value3) 110 | self.assertTrue(result) 111 | result = yield self.client.smembers(key) 112 | self.assertListEqual(sorted(result), sorted([value1, value2, value3])) 113 | 114 | @testing.gen_test 115 | def test_smove(self): 116 | key1, key2, value1 = self.uuid4(3) 117 | result = yield self.client.sadd(key1, value1) 118 | self.assertTrue(result) 119 | result = yield self.client.smove(key1, key2, value1) 120 | self.assertTrue(result) 121 | result = yield self.client.sismember(key1, value1) 122 | self.assertFalse(result) 123 | result = yield self.client.sismember(key2, value1) 124 | self.assertTrue(result) 125 | 126 | @testing.gen_test 127 | def test_spop(self): 128 | key, value1, value2, value3 = self.uuid4(4) 129 | values = [value1, value2, value3] 130 | result = yield self.client.sadd(key, *values) 131 | self.assertTrue(result) 132 | member = yield self.client.spop(key) 133 | self.assertIn(member, values) 134 | members = yield self.client.smembers(key) 135 | self.assertNotIn(member, members) 136 | 137 | @testing.gen_test 138 | def test_srandmember(self): 139 | key, value1, value2, value3 = self.uuid4(4) 140 | values = [value1, value2, value3] 141 | result = yield self.client.sadd(key, *values) 142 | self.assertTrue(result) 143 | member = yield self.client.srandmember(key) 144 | self.assertIn(member, values) 145 | members = yield self.client.smembers(key) 146 | self.assertIn(member, members) 147 | 148 | @testing.gen_test 149 | def test_srandmember_multi(self): 150 | key, value1, value2, value3 = self.uuid4(4) 151 | values = [value1, value2, value3] 152 | result = yield self.client.sadd(key, *values) 153 | self.assertTrue(result) 154 | members = yield self.client.srandmember(key, 2) 155 | for member in members: 156 | self.assertIn(member, values) 157 | self.assertEqual(len(members), 2) 158 | 159 | @testing.gen_test 160 | def test_srem(self): 161 | key, value1, value2, value3 = self.uuid4(4) 162 | values = [value1, value2, value3] 163 | result = yield self.client.sadd(key, *values) 164 | self.assertTrue(result) 165 | result = yield self.client.srem(key, value2, value3) 166 | self.assertTrue(result) 167 | members = yield self.client.smembers(key) 168 | self.assertNotIn(value2, members) 169 | self.assertNotIn(value3, members) 170 | 171 | @testing.gen_test 172 | def test_srem_dupe(self): 173 | key = self.uuid4() 174 | key, value1, value2, value3 = self.uuid4(4) 175 | values = [value1, value2, value3] 176 | result = yield self.client.sadd(key, *values) 177 | self.assertTrue(result) 178 | result = yield self.client.srem(key, value2, value3, value3) 179 | self.assertEqual(result, 2) 180 | members = yield self.client.smembers(key) 181 | self.assertNotIn(value2, members) 182 | self.assertNotIn(value3, members) 183 | 184 | @testing.gen_test 185 | def test_srem_with_error(self): 186 | key, value = self.uuid4(2) 187 | self._execute_result = exceptions.RedisError('Test Exception') 188 | with mock.patch.object(self.client, '_execute', self._execute): 189 | with self.assertRaises(exceptions.RedisError): 190 | yield self.client.srem(key, value) 191 | 192 | @testing.gen_test 193 | def test_sscan(self): 194 | key, value1, value2, value3 = self.uuid4(4) 195 | values = [value1, value2, value3] 196 | result = yield self.client.sadd(key, *values) 197 | self.assertTrue(result) 198 | cursor, result = yield self.client.sscan(key, 0) 199 | self.assertListEqual(sorted(result), sorted(values)) 200 | self.assertEqual(cursor, 0) 201 | 202 | @testing.gen_test 203 | def test_sscan_with_pattern(self): 204 | key, value1, value2, value3 = self.uuid4(4) 205 | values = [value1, value2, value3] 206 | result = yield self.client.sadd(key, *values) 207 | self.assertTrue(result) 208 | cursor, result = yield self.client.sscan(key, 0, '*') 209 | self.assertListEqual(sorted(result), sorted(values)) 210 | self.assertEqual(cursor, 0) 211 | 212 | @testing.gen_test 213 | def test_sscan_with_pattern_and_count(self): 214 | key, value1, value2, value3 = self.uuid4(4) 215 | values = [value1, value2, value3] 216 | result = yield self.client.sadd(key, *values) 217 | self.assertTrue(result) 218 | cursor, result = yield self.client.sscan(key, 0, '*', 10) 219 | self.assertListEqual(sorted(result), sorted(values)) 220 | self.assertEqual(cursor, 0) 221 | 222 | @testing.gen_test 223 | def test_sscan_with_error(self): 224 | key = self.uuid4() 225 | self._execute_result = exceptions.RedisError('Test Exception') 226 | with mock.patch.object(self.client, '_execute', self._execute): 227 | with self.assertRaises(exceptions.RedisError): 228 | yield self.client.sscan(key, 0) 229 | 230 | @testing.gen_test 231 | def test_sunion(self): 232 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 233 | result = yield self.client.sadd(key1, value1, value2) 234 | self.assertTrue(result) 235 | result = yield self.client.sadd(key2, value2, value3) 236 | self.assertTrue(result) 237 | result = yield self.client.sunion(key1, key2) 238 | self.assertListEqual(sorted(result), sorted([value1, value2, value3])) 239 | 240 | @testing.gen_test 241 | def test_suinionstore(self): 242 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 243 | result = yield self.client.sadd(key1, value1, value2) 244 | self.assertTrue(result) 245 | result = yield self.client.sadd(key2, value2, value3) 246 | self.assertTrue(result) 247 | result = yield self.client.sunionstore(key3, key1, key2) 248 | self.assertEqual(result, 3) 249 | result = yield self.client.sismember(key3, value1) 250 | self.assertTrue(result) 251 | result = yield self.client.sismember(key3, value2) 252 | self.assertTrue(result) 253 | result = yield self.client.sismember(key3, value3) 254 | self.assertTrue(result) 255 | -------------------------------------------------------------------------------- /tests/sortedsets_tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from tornado import testing 4 | 5 | from tredis import exceptions 6 | 7 | from . import base 8 | 9 | 10 | class SortedSetTests(base.AsyncTestCase): 11 | @testing.gen_test 12 | def test_zadd_single(self): 13 | key, value = self.uuid4(2) 14 | result = yield self.client.zadd(key, '1', value) 15 | self.assertEqual(result, 1) 16 | 17 | @testing.gen_test 18 | def test_zadd_multiple(self): 19 | key, value1, value2, value3 = self.uuid4(4) 20 | result = yield self.client.zadd(key, '1', value1, '2', value2, 21 | '3', value3) 22 | self.assertEqual(result, 3) 23 | 24 | @testing.gen_test 25 | def test_zadd_dict(self): 26 | key, value1, value2, value3 = self.uuid4(4) 27 | result = yield self.client.zadd(key, {'1': value1, '2': value2, 28 | '3': value3}) 29 | self.assertEqual(result, 3) 30 | 31 | @testing.gen_test 32 | def test_zadd_multiple_dupe(self): 33 | key, value1, value2, value3 = self.uuid4(4) 34 | result = yield self.client.zadd(key, '1', value1, '2', value2, 35 | '3', value3, '4', value3) 36 | self.assertEqual(result, 3) 37 | 38 | @testing.gen_test 39 | def test_zadd_ch(self): 40 | key, value1, value2, value3 = self.uuid4(4) 41 | result = yield self.client.zadd(key, '1', value1, '2', value2) 42 | self.assertEqual(result, 2) 43 | result = yield self.client.zadd(key, '2', value1, '3', value2, 44 | '4', value3, ch=True) 45 | self.assertEqual(result, 3) 46 | 47 | @testing.gen_test 48 | def test_zadd_xx(self): 49 | key, value1, value2, value3 = self.uuid4(4) 50 | result = yield self.client.zadd(key, '1', value1, '2', value2) 51 | self.assertEqual(result, 2) 52 | result = yield self.client.zadd(key, '2', value1, '3', value2, 53 | '4', value3, xx=True) 54 | self.assertEqual(result, 0) 55 | 56 | @testing.gen_test 57 | def test_zadd_nx(self): 58 | key, value1, value2, value3 = self.uuid4(4) 59 | result = yield self.client.zadd(key, '1', value1, '2', value2) 60 | self.assertEqual(result, 2) 61 | result = yield self.client.zadd(key, '2', value1, '3', value2, 62 | '4', value3, nx=True, ch=True) 63 | self.assertEqual(result, 1) 64 | 65 | @testing.gen_test 66 | def test_zadd_incr(self): 67 | key, value = self.uuid4(2) 68 | result = yield self.client.zadd(key, '1', value) 69 | self.assertEqual(result, 1) 70 | result = yield self.client.zadd(key, '10', value, incr=True) 71 | self.assertEqual(result, b'11') 72 | 73 | @testing.gen_test 74 | def test_zadd_with_error(self): 75 | key, score, value = self.uuid4(3) 76 | self._execute_result = exceptions.RedisError('Test Exception') 77 | with mock.patch.object(self.client, '_execute', self._execute): 78 | with self.assertRaises(exceptions.RedisError): 79 | yield self.client.zadd(key, score, value) 80 | 81 | @testing.gen_test 82 | def test_zcard_with_extant_set(self): 83 | key, value1, value2, value3 = self.uuid4(4) 84 | result = yield self.client.zadd(key, '1', value1, '2', value2, 85 | '3', value3) 86 | self.assertEqual(result, 3) 87 | result = yield self.client.zcard(key) 88 | self.assertEqual(result, 3) 89 | 90 | @testing.gen_test 91 | def test_zcard_with_nonextant_set(self): 92 | key = self.uuid4() 93 | result = yield self.client.zcard(key) 94 | self.assertEqual(result, 0) 95 | 96 | @testing.gen_test 97 | def test_zrangebyscore(self): 98 | key, value1, value2, value3 = self.uuid4(4) 99 | result = yield self.client.zadd(key, '1', value1, '2', value2, 100 | '3', value3) 101 | self.assertEqual(result, 3) 102 | result = yield self.client.zrangebyscore(key, '1', '2') 103 | self.assertListEqual(result, [value1, value2]) 104 | 105 | @testing.gen_test 106 | def test_zrangebyscore_withitems(self): 107 | key, value1, value2, value3 = self.uuid4(4) 108 | result = yield self.client.zadd(key, '1', value1, '2', value2, 109 | '3', value3) 110 | self.assertEqual(result, 3) 111 | result = yield self.client.zrangebyscore(key, '1', '2', 112 | with_scores=True) 113 | self.assertListEqual(result, [value1, b'1', value2, b'2']) 114 | 115 | @testing.gen_test 116 | def test_zrangebyscore_offset(self): 117 | key, value1, value2, value3 = self.uuid4(4) 118 | result = yield self.client.zadd(key, '1', value1, '2', value2, 119 | '3', value3) 120 | self.assertEqual(result, 3) 121 | result = yield self.client.zrangebyscore(key, '1', '2', 122 | offset=1, count=20) 123 | self.assertListEqual(result, [value2]) 124 | 125 | @testing.gen_test 126 | def test_zrangebyscore_count(self): 127 | key, value1, value2, value3 = self.uuid4(4) 128 | result = yield self.client.zadd(key, '1', value1, '2', value2, 129 | '3', value3) 130 | self.assertEqual(result, 3) 131 | result = yield self.client.zrangebyscore(key, '1', '3', 132 | offset=0, count=1) 133 | self.assertListEqual(result, [value1]) 134 | 135 | @testing.gen_test 136 | def test_zremrangebyscore(self): 137 | key, value1, value2, value3 = self.uuid4(4) 138 | result = yield self.client.zadd(key, '1', value1, '2', value2, 139 | '3', value3) 140 | self.assertEqual(result, 3) 141 | result = yield self.client.zremrangebyscore(key, '1', '2') 142 | self.assertEqual(result, 2) 143 | 144 | @testing.gen_test 145 | def test_zremrangebyscore_inf(self): 146 | key, value1, value2, value3 = self.uuid4(4) 147 | result = yield self.client.zadd(key, '1', value1, '2', value2, 148 | '3', value3) 149 | self.assertEqual(result, 3) 150 | result = yield self.client.zremrangebyscore(key, '(1', 'inf') 151 | self.assertEqual(result, 2) 152 | 153 | @testing.gen_test 154 | def test_zscore_with_member_of_set(self): 155 | key, value1, value2, value3 = self.uuid4(4) 156 | result = yield self.client.zadd(key, '1', value1, '2', value2, 157 | '3', value3) 158 | self.assertEqual(result, 3) 159 | result = yield self.client.zscore(key, value1) 160 | self.assertEqual(result, b'1') 161 | 162 | @testing.gen_test 163 | def test_zscore_with_nonmember_of_set(self): 164 | key, value1 = self.uuid4(2) 165 | result = yield self.client.zscore(key, value1) 166 | self.assertEqual(result, None) 167 | -------------------------------------------------------------------------------- /tests/strings_tests.py: -------------------------------------------------------------------------------- 1 | from tornado import gen 2 | from tornado import testing 3 | 4 | import tredis 5 | 6 | from . import base 7 | 8 | 9 | class StringTests(base.AsyncTestCase): 10 | 11 | @testing.gen_test 12 | def test_append(self): 13 | key, value1, value2 = self.uuid4(3) 14 | result = yield self.expiring_set(key, value1) 15 | self.assertTrue(result) 16 | result = yield self.client.append(key, value2) 17 | self.assertTrue(result) 18 | result = yield self.client.get(key) 19 | self.assertEqual(result, value1 + value2) 20 | 21 | @testing.gen_test 22 | def test_append_of_non_existent_value(self): 23 | key, value = self.uuid4(2) 24 | result = yield self.client.append(key, value) 25 | self.assertTrue(result) 26 | 27 | @testing.gen_test 28 | def test_bitcount(self): 29 | key = self.uuid4(1) 30 | result = yield self.expiring_set(key, 'foobar') 31 | self.assertTrue(result) 32 | result = yield self.client.bitcount(key) 33 | self.assertEqual(result, 26) 34 | 35 | @testing.gen_test 36 | def test_bitcount_with_start_and_not_end(self): 37 | key = self.uuid4(1) 38 | with self.assertRaises(ValueError): 39 | yield self.client.bitcount(key, 1) 40 | 41 | @testing.gen_test 42 | def test_bitcount_without_start_and_with_end(self): 43 | key = self.uuid4(1) 44 | with self.assertRaises(ValueError): 45 | yield self.client.bitcount(key, end=1) 46 | 47 | @testing.gen_test 48 | def test_bitcount_with_start_and_end(self): 49 | key = self.uuid4(1) 50 | result = yield self.expiring_set(key, 'foobar') 51 | self.assertTrue(result) 52 | result = yield self.client.bitcount(key, 1, -1) 53 | self.assertEqual(result, 22) 54 | 55 | @testing.gen_test 56 | def test_bitop_and(self): 57 | key1, key2, key3 = self.uuid4(3) 58 | result = yield self.expiring_set(key1, 'deadbeef') 59 | self.assertTrue(result) 60 | result = yield self.expiring_set(key2, '8badf00d') 61 | self.assertTrue(result) 62 | result = yield self.client.bitop(b'AND', key3, key1, key2) 63 | self.assertEqual(result, 8) 64 | result = yield self.client.get(key3) 65 | self.assertEqual(result, b' `adb d') 66 | 67 | @testing.gen_test 68 | def test_bitop_and_const(self): 69 | key1, key2, key3 = self.uuid4(3) 70 | result = yield self.expiring_set(key1, 'deadbeef') 71 | self.assertTrue(result) 72 | result = yield self.expiring_set(key2, '8badf00d') 73 | self.assertTrue(result) 74 | result = yield self.client.bitop(tredis.BITOP_AND, key3, key1, key2) 75 | self.assertEqual(result, 8) 76 | result = yield self.client.get(key3) 77 | self.assertEqual(result, b' `adb d') 78 | 79 | @testing.gen_test 80 | def test_bitop_or(self): 81 | key1, key2, key3 = self.uuid4(3) 82 | result = yield self.expiring_set(key1, 'deadbeef') 83 | self.assertTrue(result) 84 | result = yield self.expiring_set(key2, '8badf00d') 85 | self.assertTrue(result) 86 | result = yield self.client.bitop(b'OR', key3, key1, key2) 87 | self.assertEqual(result, 8) 88 | result = yield self.client.get(key3) 89 | self.assertEqual(result, b'|gadfuuf') 90 | 91 | @testing.gen_test 92 | def test_bitop_or_const(self): 93 | key1, key2, key3 = self.uuid4(3) 94 | result = yield self.expiring_set(key1, 'deadbeef') 95 | self.assertTrue(result) 96 | result = yield self.expiring_set(key2, '8badf00d') 97 | self.assertTrue(result) 98 | result = yield self.client.bitop(tredis.BITOP_OR, key3, key1, key2) 99 | self.assertEqual(result, 8) 100 | result = yield self.client.get(key3) 101 | self.assertEqual(result, b'|gadfuuf') 102 | 103 | @testing.gen_test 104 | def test_bitop_xor(self): 105 | key1, key2, key3 = self.uuid4(3) 106 | result = yield self.expiring_set(key1, 'deadbeef') 107 | self.assertTrue(result) 108 | result = yield self.expiring_set(key2, '8badf00d') 109 | self.assertTrue(result) 110 | result = yield self.client.bitop(b'XOR', key3, key1, key2) 111 | self.assertEqual(result, 8) 112 | result = yield self.client.get(key3) 113 | self.assertEqual(result, b'\\\x07\x00\x00\x04UU\x02') 114 | 115 | @testing.gen_test 116 | def test_bitop_xor_const(self): 117 | key1, key2, key3 = self.uuid4(3) 118 | result = yield self.expiring_set(key1, 'deadbeef') 119 | self.assertTrue(result) 120 | result = yield self.expiring_set(key2, '8badf00d') 121 | self.assertTrue(result) 122 | result = yield self.client.bitop(tredis.BITOP_XOR, key3, key1, key2) 123 | self.assertEqual(result, 8) 124 | result = yield self.client.get(key3) 125 | self.assertEqual(result, b'\\\x07\x00\x00\x04UU\x02') 126 | 127 | @testing.gen_test 128 | def test_bitop_not(self): 129 | key1, key2 = self.uuid4(2) 130 | result = yield self.expiring_set(key1, 'deadbeef') 131 | self.assertTrue(result) 132 | result = yield self.client.bitop(b'NOT', key2, key1) 133 | self.assertEqual(result, 8) 134 | result = yield self.client.get(key2) 135 | self.assertEqual(result, b'\x9b\x9a\x9e\x9b\x9d\x9a\x9a\x99') 136 | 137 | @testing.gen_test 138 | def test_bitop_not_const(self): 139 | key1, key2 = self.uuid4(2) 140 | result = yield self.expiring_set(key1, 'deadbeef') 141 | self.assertTrue(result) 142 | result = yield self.client.bitop(tredis.BITOP_NOT, key2, key1) 143 | self.assertEqual(result, 8) 144 | result = yield self.client.get(key2) 145 | self.assertEqual(result, b'\x9b\x9a\x9e\x9b\x9d\x9a\x9a\x99') 146 | 147 | @testing.gen_test 148 | def test_bitop_not_invalid_operation(self): 149 | key1, key2 = self.uuid4(2) 150 | with self.assertRaises(ValueError): 151 | yield self.client.bitop(b'YO!', key2, key1, key1) 152 | 153 | @testing.gen_test 154 | def test_bitop_not_too_many_keys(self): 155 | key1, key2 = self.uuid4(2) 156 | with self.assertRaises(ValueError): 157 | yield self.client.bitop(tredis.BITOP_NOT, key2, key1, key1) 158 | 159 | @testing.gen_test 160 | def test_bitpos(self): 161 | key = self.uuid4(1) 162 | result = self.expiring_set(key, b'\xff\xf0\x00') 163 | self.assertTrue(result) 164 | result = yield self.client.bitpos(key, 0) 165 | self.assertEqual(result, 12) 166 | 167 | @testing.gen_test 168 | def test_bitpos_invalid_bit(self): 169 | key = self.uuid4(1) 170 | with self.assertRaises(ValueError): 171 | yield self.client.bitpos(key, 2) 172 | 173 | @testing.gen_test 174 | def test_bitpos_with_start_and_end(self): 175 | key = self.uuid4(1) 176 | result = self.expiring_set(key, b'\xff\xf0\xf0') 177 | self.assertTrue(result) 178 | result = yield self.client.bitpos(key, 1, 1, 1) 179 | self.assertEqual(result, 8) 180 | 181 | @testing.gen_test 182 | def test_bitpos_with_start_and_not_end(self): 183 | key = self.uuid4(1) 184 | with self.assertRaises(ValueError): 185 | yield self.client.bitpos(key, 0, 1) 186 | 187 | @testing.gen_test 188 | def test_bitpos_without_start_and_with_end(self): 189 | key = self.uuid4(1) 190 | with self.assertRaises(ValueError): 191 | yield self.client.bitpos(key, 0, end=1) 192 | 193 | @testing.gen_test 194 | def test_decr(self): 195 | key = self.uuid4() 196 | result = yield self.expiring_set(key, b'10') 197 | self.assertTrue(result) 198 | result = yield self.client.decr(key) 199 | self.assertEqual(result, 9) 200 | result = yield self.client.decr(key) 201 | self.assertEqual(result, 8) 202 | result = yield self.client.get(key) 203 | self.assertEqual(int(result), 8) 204 | 205 | @testing.gen_test 206 | def test_decrby(self): 207 | key = self.uuid4() 208 | result = yield self.expiring_set(key, b'10') 209 | self.assertTrue(result) 210 | result = yield self.client.decrby(key, 2) 211 | self.assertEqual(result, 8) 212 | result = yield self.client.decrby(key, 3) 213 | self.assertEqual(result, 5) 214 | result = yield self.client.get(key) 215 | self.assertEqual(int(result), 5) 216 | 217 | @testing.gen_test 218 | def test_getbit(self): 219 | key = self.uuid4() 220 | result = yield self.expiring_set(key, b'\x0f') 221 | self.assertTrue(result) 222 | result = yield self.client.getbit(key, 4) 223 | self.assertEqual(result, 1) 224 | 225 | @testing.gen_test 226 | def test_getrange(self): 227 | key, value = self.uuid4(2) 228 | result = yield self.expiring_set(key, value) 229 | self.assertTrue(result) 230 | result = yield self.client.getrange(key, 2, -3) 231 | self.assertEqual(result, value[2:-2]) 232 | 233 | @testing.gen_test 234 | def test_getset(self): 235 | key, value1, value2 = self.uuid4(3) 236 | result = yield self.expiring_set(key, value1) 237 | self.assertTrue(result) 238 | result = yield self.client.getset(key, value2) 239 | self.assertEquals(result, value1) 240 | result = yield self.client.get(key) 241 | self.assertEquals(result, value2) 242 | 243 | @testing.gen_test 244 | def test_incr(self): 245 | key = self.uuid4() 246 | result = yield self.client.incr(key) 247 | self.assertEqual(result, 1) 248 | result = yield self.client.incr(key) 249 | self.assertEqual(result, 2) 250 | result = yield self.client.get(key) 251 | self.assertEqual(int(result), 2) 252 | result = yield self.client.delete(key) 253 | self.assertTrue(result) 254 | 255 | @testing.gen_test 256 | def test_incrby(self): 257 | key = self.uuid4() 258 | result = yield self.expiring_set(key, b'10') 259 | self.assertTrue(result) 260 | result = yield self.client.incrby(key, 2) 261 | self.assertEqual(result, 12) 262 | result = yield self.client.incrby(key, 3) 263 | self.assertEqual(result, 15) 264 | result = yield self.client.get(key) 265 | self.assertEqual(int(result), 15) 266 | 267 | @testing.gen_test 268 | def test_incrbyfloat(self): 269 | key = self.uuid4() 270 | result = yield self.expiring_set(key, b'10.50') 271 | self.assertTrue(result) 272 | result = yield self.client.incrbyfloat(key, 0.1) 273 | self.assertEqual(result, b'10.6') 274 | result = yield self.client.get(key) 275 | self.assertEqual(result, b'10.6') 276 | 277 | @testing.gen_test 278 | def test_mget(self): 279 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 280 | result = yield self.expiring_set(key1, value1) 281 | self.assertTrue(result) 282 | result = yield self.expiring_set(key2, value2) 283 | self.assertTrue(result) 284 | result = yield self.expiring_set(key3, value3) 285 | self.assertTrue(result) 286 | result = yield self.client.mget(key1, key2, key3) 287 | self.assertListEqual([value1, value2, value3], result) 288 | 289 | @testing.gen_test 290 | def test_mset(self): 291 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 292 | values = {key1: value1, 293 | key2: value2, 294 | key3: value3} 295 | result = yield self.client.mset(values) 296 | self.assertTrue(result) 297 | result = yield self.client.mget(key1, key2, key3) 298 | self.assertListEqual([value1, value2, value3], result) 299 | 300 | @testing.gen_test 301 | def test_msetnx(self): 302 | key1, key2, key3, value1, value2, value3 = self.uuid4(6) 303 | values = {key1: value1, 304 | key2: value2, 305 | key3: value3} 306 | result = yield self.client.msetnx(values) 307 | self.assertTrue(result) 308 | result = yield self.client.mget(key1, key2, key3) 309 | self.assertListEqual([value1, value2, value3], result) 310 | 311 | @testing.gen_test 312 | def test_msetnx_fail(self): 313 | key1, key2, key3, value1, value2, value3, value4 = self.uuid4(7) 314 | result = self.expiring_set(key1, value4) 315 | self.assertTrue(result) 316 | values = {key1: value1, 317 | key2: value2, 318 | key3: value3} 319 | result = yield self.client.msetnx(values) 320 | self.assertFalse(result) 321 | 322 | @testing.gen_test 323 | def test_psetex(self): 324 | key, value = self.uuid4(2) 325 | result = yield self.client.psetex(key, 100, value) 326 | self.assertTrue(result) 327 | result = yield self.client.get(key) 328 | self.assertEqual(result, value) 329 | yield gen.sleep(0.300) 330 | result = yield self.client.get(key) 331 | self.assertIsNone(result) 332 | 333 | @testing.gen_test 334 | def test_setbit(self): 335 | key = self.uuid4() 336 | result = yield self.client.setbit(key, 100, 1) 337 | self.assertEqual(result, 0) 338 | result = yield self.client.getbit(key, 100) 339 | self.assertEqual(result, 1) 340 | result = yield self.client.setbit(key, 100, 0) 341 | self.assertEqual(result, 1) 342 | 343 | @testing.gen_test 344 | def test_setbit_invalid_bit(self): 345 | with self.assertRaises(ValueError): 346 | key = self.uuid4() 347 | yield self.client.setbit(key, 100, 2) 348 | 349 | @testing.gen_test 350 | def test_setex(self): 351 | key, value = self.uuid4(2) 352 | result = yield self.client.setex(key, 1, value) 353 | self.assertTrue(result) 354 | result = yield self.client.get(key) 355 | self.assertEqual(result, value) 356 | yield gen.sleep(1.0) 357 | result = yield self.client.get(key) 358 | self.assertIsNone(result) 359 | 360 | @testing.gen_test 361 | def test_setnx(self): 362 | key, value = self.uuid4(2) 363 | result = yield self.client.setnx(key, value) 364 | self.assertTrue(result) 365 | result = yield self.client.setnx(key, value) 366 | self.assertFalse(result) 367 | result = yield self.client.delete(key) 368 | self.assertTrue(result) 369 | 370 | @testing.gen_test 371 | def test_simple_set_and_get(self): 372 | key, value = self.uuid4(2) 373 | result = yield self.expiring_set(key, value) 374 | self.assertTrue(result) 375 | result = yield self.client.get(key) 376 | self.assertEqual(result, value) 377 | 378 | @testing.gen_test 379 | def test_simple_set_int_and_get(self): 380 | key = self.uuid4() 381 | result = yield self.expiring_set(key, 2) 382 | self.assertTrue(result) 383 | result = yield self.client.get(key) 384 | self.assertEqual(result, b'2') 385 | 386 | @testing.gen_test 387 | def test_simple_set_str_and_get(self): 388 | key = self.uuid4() 389 | result = yield self.expiring_set(key, 'hi') 390 | self.assertTrue(result) 391 | result = yield self.client.get(key) 392 | self.assertEqual(result, b'hi') 393 | 394 | @testing.gen_test 395 | def test_simple_set_invalid_type(self): 396 | key = self.uuid4() 397 | with self.assertRaises(ValueError): 398 | yield self.expiring_set(key, {}) 399 | 400 | @testing.gen_test 401 | def test_set_ex(self): 402 | key, value = self.uuid4(2) 403 | result = yield self.client.set(key, value, ex=1) 404 | self.assertTrue(result) 405 | result = yield self.client.get(key) 406 | self.assertEqual(result, value) 407 | yield gen.sleep(1.0) 408 | result = yield self.client.get(key) 409 | self.assertIsNone(result) 410 | 411 | @testing.gen_test 412 | def test_set_px(self): 413 | key, value = self.uuid4(2) 414 | result = yield self.client.set(key, value, px=100) 415 | self.assertTrue(result) 416 | result = yield self.client.get(key) 417 | self.assertEqual(result, value) 418 | yield gen.sleep(0.300) 419 | result = yield self.client.get(key) 420 | self.assertIsNone(result) 421 | 422 | @testing.gen_test 423 | def test_set_nx(self): 424 | key, value = self.uuid4(2) 425 | result = yield self.expiring_set(key, value, nx=True) 426 | self.assertTrue(result) 427 | result = yield self.client.get(key) 428 | self.assertEqual(result, value) 429 | 430 | @testing.gen_test 431 | def test_set_nx_with_value(self): 432 | key, value = self.uuid4(2) 433 | result = yield self.expiring_set(key, value, nx=True) 434 | self.assertTrue(result) 435 | result = yield self.client.get(key) 436 | self.assertEqual(result, value) 437 | result = yield self.expiring_set(key, value, nx=True) 438 | self.assertFalse(result) 439 | 440 | @testing.gen_test 441 | def test_set_xx_with_value(self): 442 | key, value = self.uuid4(2) 443 | result = yield self.expiring_set(key, value) 444 | self.assertTrue(result) 445 | result = yield self.client.get(key) 446 | self.assertEqual(result, value) 447 | result = yield self.expiring_set(key, value, xx=True) 448 | self.assertTrue(result) 449 | 450 | @testing.gen_test 451 | def test_set_xx_without_value(self): 452 | key, value = self.uuid4(2) 453 | result = yield self.expiring_set(key, value, xx=True) 454 | self.assertFalse(result) 455 | 456 | @testing.gen_test 457 | def test_setrange(self): 458 | key, value1, value2 = self.uuid4(3) 459 | result = yield self.expiring_set(key, value1) 460 | self.assertTrue(result) 461 | result = yield self.client.setrange(key, 4, value2) 462 | self.assertEqual(result, len(value1) + 4) 463 | result = yield self.client.get(key) 464 | self.assertEqual(result, value1[0:4] + value2) 465 | 466 | @testing.gen_test 467 | def test_strlen(self): 468 | key, value = self.uuid4(2) 469 | result = yield self.expiring_set(key, value) 470 | self.assertTrue(result) 471 | result = yield self.client.strlen(key) 472 | self.assertTrue(result, len(value)) 473 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34 3 | toxworkdir = build/tox 4 | 5 | [testenv] 6 | deps = -rtest-requirements.txt 7 | commands = nosetests 8 | -------------------------------------------------------------------------------- /tredis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TRedis 3 | ====== 4 | An asynchronous Redis client for Tornado 5 | 6 | """ 7 | from tredis.client import Client, RedisClient 8 | from tredis.exceptions import * 9 | from tredis.strings import BITOP_AND, BITOP_OR, BITOP_XOR, BITOP_NOT 10 | 11 | __version__ = '0.8.0' 12 | -------------------------------------------------------------------------------- /tredis/cluster.py: -------------------------------------------------------------------------------- 1 | """Redis Cluster Commands Mixin""" 2 | import collections 3 | 4 | from tredis import common 5 | 6 | ClusterNode = collections.namedtuple('ClusterNode', [ 7 | 'id', 'ip', 'port', 'flags', 'master', 'ping_sent', 'pong_recv', 8 | 'config_epoch', 'link_state', 'slots' 9 | ]) 10 | """:class:`tredis.cluster.ClusterNode` is a :class:`~collections.namedtuple` 11 | that contains the attributes for a single node returned by the 12 | ``CLUSTER NODES`` command. 13 | 14 | .. versionadded: 0.7 15 | 16 | :param bytes id: The node ID 17 | :param bytes ip: The IP address of the node 18 | :param int port: The node TCP port 19 | :param bytes flags: A list of comma separated flags: ``myself``, ``master``, 20 | ``slave``, ``fail?``, ``fail``, ``handshake``, ``noaddr``, ``noflags``. 21 | :param bytes master: If the node is a slave, and the master is known, the master 22 | node ID, otherwise the ``-`` character. 23 | :param int ping_sent: Milliseconds unix time at which the currently active ping 24 | was sent, or zero if there are no pending pings. 25 | :param int pong_recv: Milliseconds unix time the last pong was received. 26 | :param int config_epoch: The configuration epoch (or version) of the current 27 | node (or of the current master if the node is a slave). Each time there is 28 | a failover, a new, unique, monotonically increasing configuration epoch is 29 | created. If multiple nodes claim to serve the same hash slots, the one with 30 | higher configuration epoch wins. 31 | :param bytes link_state: The state of the link used for the node-to-node cluster 32 | bus. We use this link to communicate with the node. Can be ``connected`` or 33 | ``disconnected``. 34 | :param slots: A hash slot number or range. There may be up to 16384 entries in 35 | total (limit never reached). This is the list of hash slots served by this 36 | node. If the entry is just a number, is parsed as such. If it is a range, 37 | it is in the form start-end, and means that the node is responsible for 38 | all the hash slots from start to end including the start and end values. 39 | :type slots: list(tuple(int, int)) 40 | 41 | """ 42 | 43 | 44 | class ClusterMixin(object): 45 | """Redis Cluster Commands Mixin""" 46 | 47 | def cluster_add_slots(self, *slots): 48 | pass 49 | 50 | def cluster_count_failure_report(self, node): 51 | pass 52 | 53 | def cluster_count_keys_in_slot(self, slot): 54 | pass 55 | 56 | def cluster_del_slots(self, slot): 57 | pass 58 | 59 | def cluster_failover(self, method): 60 | pass # force, takeover 61 | 62 | def cluster_forget(self, node_id): 63 | pass 64 | 65 | def cluster_get_keys_in_slot(self, slot, count): 66 | pass 67 | 68 | def cluster_info(self): 69 | """``CLUSTER INFO`` provides ``INFO`` style information about Redis 70 | Cluster vital parameters. 71 | 72 | .. versionadded:: 0.7.0 73 | 74 | :returns: A dictionary of current cluster information 75 | :rtype: dict 76 | 77 | :key cluster_state: State is ok if the node is able to receive 78 | queries. fail if there is at least one hash slot which is unbound 79 | (no node associated), in error state (node serving it is flagged 80 | with ``FAIL`` flag), or if the majority of masters can't be 81 | reached by this node. 82 | :key cluster_slots_assigned: Number of slots which are associated to 83 | some node (not unbound). This number should be ``16384`` for the 84 | node to work properly, which means that each hash slot should be 85 | mapped to a node. 86 | :key cluster_slots_ok: Number of hash slots mapping to a node not in 87 | ``FAIL`` or ``PFAIL`` state. 88 | :key cluster_slots_pfail: Number of hash slots mapping to a node in 89 | ``PFAIL`` state. Note that those hash slots still work 90 | correctly, as long as the ``PFAIL`` state is not promoted to 91 | ``FAIL`` by the failure detection algorithm. ``PFAIL`` 92 | only means that we are currently not able to talk with the node, 93 | but may be just a transient error. 94 | :key cluster_slots_fail: Number of hash slots mapping to a node in 95 | ``FAIL`` state. If this number is not zero the node is not 96 | able to serve queries unless cluster-require-full-coverage is set 97 | to no in the configuration. 98 | :key cluster_known_nodes: The total number of known nodes in the 99 | cluster, including nodes in ``HANDSHAKE`` state that may not 100 | currently be proper members of the cluster. 101 | :key cluster_size: The number of master nodes serving at least one 102 | hash slot in the cluster. 103 | :key cluster_current_epoch: The local Current Epoch variable. This is 104 | used in order to create unique increasing version numbers during 105 | fail overs. 106 | :key cluster_my_epoch: The Config Epoch of the node we are talking 107 | with. This is the current configuration version assigned to this 108 | node. 109 | :key cluster_stats_messages_sent: Number of messages sent via the 110 | cluster node-to-node binary bus. 111 | :key cluster_stats_messages_received: Number of messages received via 112 | the cluster node-to-node binary bus. 113 | :raises: :exc:`~tredis.exceptions.RedisError` 114 | 115 | """ 116 | return self._execute( 117 | [b'CLUSTER', 'INFO'], format_callback=common.format_info_response) 118 | 119 | def cluster_key_slot(self, key): 120 | pass 121 | 122 | def cluster_meet(self, ip, port): 123 | pass 124 | 125 | def cluster_nodes(self): 126 | """Each node in a Redis Cluster has its view of the current cluster 127 | configuration, given by the set of known nodes, the state of the 128 | connection we have with such nodes, their flags, properties and 129 | assigned slots, and so forth. 130 | 131 | ``CLUSTER NODES`` provides all this information, that is, the current 132 | cluster configuration of the node we are contacting, in a serialization 133 | format which happens to be exactly the same as the one used by Redis 134 | Cluster itself in order to store on disk the cluster state (however the 135 | on disk cluster state has a few additional info appended at the end). 136 | 137 | Note that normally clients willing to fetch the map between Cluster 138 | hash slots and node addresses should use ``CLUSTER SLOTS`` instead. 139 | ``CLUSTER NODES``, that provides more information, should be used for 140 | administrative tasks, debugging, and configuration inspections. It is 141 | also used by ``redis-trib`` in order to manage a cluster. 142 | 143 | .. versionadded:: 0.7.0 144 | 145 | :rtype: list(:class:`~tredis.cluster.ClusterNode`) 146 | :raises: :exc:`~tredis.exceptions.RedisError` 147 | 148 | """ 149 | 150 | def format_response(result): 151 | values = [] 152 | for row in result.decode('utf-8').split('\n'): 153 | if not row: 154 | continue 155 | parts = row.split(' ') 156 | slots = [] 157 | for slot in parts[8:]: 158 | if '-' in slot: 159 | sparts = slot.split('-') 160 | slots.append((int(sparts[0]), int(sparts[1]))) 161 | else: 162 | slots.append((int(slot), int(slot))) 163 | ip_port = common.split_connection_host_port(parts[1]) 164 | values.append( 165 | ClusterNode(parts[0], ip_port[0], ip_port[1], parts[2], 166 | parts[3], int(parts[4]), int(parts[5]), 167 | int(parts[6]), parts[7], slots)) 168 | return values 169 | 170 | return self._execute( 171 | ['CLUSTER', 'NODES'], format_callback=format_response) 172 | 173 | def cluster_replicate(self, node_id): 174 | pass 175 | 176 | def cluster_reset(self, method): 177 | pass # hard, soft 178 | 179 | def cluster_save_config(self): 180 | pass 181 | 182 | def cluster_set_config_epoch(self, config_epoch): 183 | pass 184 | 185 | def cluster_set_slot(self, subcommand, node_id): 186 | pass 187 | 188 | def cluster_slaves(self, node_id): 189 | pass 190 | 191 | def cluster_slots(self): 192 | pass 193 | 194 | def cluster_readonly(self): 195 | pass 196 | 197 | def cluster_readwrite(self): 198 | pass 199 | -------------------------------------------------------------------------------- /tredis/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common utility methods 3 | 4 | """ 5 | import logging 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | def maybe_raise_exception(future): 11 | if future.exception(): 12 | raise future.exception() 13 | 14 | 15 | def split_connection_host_port(value): 16 | parts = value.split(':') 17 | LOGGER.debug('Returning %r', (parts[0], int(parts[1]))) 18 | return parts[0], int(parts[1]) 19 | 20 | 21 | def parse_info_value(value): 22 | """ 23 | 24 | :param value: 25 | :return: 26 | 27 | """ 28 | try: 29 | if '.' in value: 30 | return float(value) 31 | else: 32 | return int(value) 33 | except ValueError: 34 | if ',' in value or '=' in value: 35 | retval = {} 36 | for row in value.split(','): 37 | key, val = row.rsplit('=', 1) 38 | retval[key] = parse_info_value(val) 39 | return retval 40 | return value 41 | 42 | 43 | def format_info_response(value): 44 | """Format the response from redis 45 | 46 | :param str value: The return response from redis 47 | :rtype: dict 48 | 49 | """ 50 | info = {} 51 | for line in value.decode('utf-8').splitlines(): 52 | if not line or line[0] == '#': 53 | continue 54 | if ':' in line: 55 | key, value = line.split(':', 1) 56 | info[key] = parse_info_value(value) 57 | return info 58 | -------------------------------------------------------------------------------- /tredis/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python 2 & 3 Compatibility Functions 3 | 4 | """ 5 | 6 | 7 | def ascii(value): 8 | """Return the string of value 9 | 10 | :param mixed value: The value to return 11 | :rtype: str 12 | 13 | """ 14 | return '{0}'.format(value) 15 | -------------------------------------------------------------------------------- /tredis/connection.py: -------------------------------------------------------------------------------- 1 | """Redis Connection Commands Mixin""" 2 | 3 | 4 | class ConnectionMixin(object): 5 | """Redis Connection Commands Mixin""" 6 | pass 7 | -------------------------------------------------------------------------------- /tredis/crc16.py: -------------------------------------------------------------------------------- 1 | """ 2 | XModem CRC 16 (CRC-CCITT) algorithm used by Redis Cluster to hash keys 3 | 4 | """ 5 | import sys 6 | 7 | _CRC16_LOOKUP = [ 8 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 9 | 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 10 | 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 11 | 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 12 | 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 13 | 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 14 | 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 15 | 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 16 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 17 | 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 18 | 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 19 | 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 20 | 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 21 | 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 22 | 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 23 | 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 24 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 25 | 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 26 | 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 27 | 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 28 | 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 29 | 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 30 | 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 31 | 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 32 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 33 | 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 34 | 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 35 | 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 36 | 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 37 | ] 38 | 39 | 40 | def _py2_crc16(value): 41 | """Calculate the CRC for the value in Python 2 42 | 43 | :param str value: The value to return for the CRC Checksum 44 | :rtype: int 45 | 46 | """ 47 | crc = 0 48 | for byte in value: 49 | crc = ((crc << 8) & 0xffff) ^ \ 50 | _CRC16_LOOKUP[((crc >> 8) ^ ord(byte)) & 0xff] 51 | return crc 52 | 53 | 54 | def _py3_crc16(value): 55 | """Calculate the CRC for the value in Python 3 56 | 57 | :param bytes value: The value to return for the CRC Checksum 58 | :rtype: int 59 | 60 | """ 61 | crc = 0 62 | for byte in value: 63 | crc = ((crc << 8) & 0xffff) ^ _CRC16_LOOKUP[((crc >> 8) ^ byte) & 0xff] 64 | return crc 65 | 66 | 67 | crc16 = _py2_crc16 if sys.version_info < (3, 0, 0) else _py3_crc16 68 | """Pick the right method based upon the Python version""" 69 | -------------------------------------------------------------------------------- /tredis/exceptions.py: -------------------------------------------------------------------------------- 1 | """TRedis Exceptions""" 2 | 3 | 4 | class TRedisException(Exception): 5 | """Raised as a top-level exception class for all exceptions raised by 6 | :class:`~tredis.RedisClient`. 7 | 8 | """ 9 | pass 10 | 11 | 12 | class ConnectError(TRedisException): 13 | """Raised when :class:`~tredis.RedisClient` can not connect to the 14 | specified Redis server. 15 | 16 | """ 17 | pass 18 | 19 | 20 | class ConnectionError(TRedisException): 21 | """Raised when :class:`~tredis.RedisClient` has had its connection to the 22 | Redis server interrupted unexpectedly. 23 | 24 | """ 25 | pass 26 | 27 | 28 | class AuthError(TRedisException): 29 | """Raised when :meth:`~tredis.RedisClient.auth` is invoked and the Redis 30 | server returns an error. 31 | 32 | """ 33 | pass 34 | 35 | 36 | class RedisError(TRedisException): 37 | """Raised when the Redis server returns a error to 38 | :class:`~tredis.RedisClient`. The string representation of this class will 39 | contain the error response from the Redis server, if one is sent. 40 | 41 | """ 42 | pass 43 | 44 | 45 | class SubscribedError(TRedisException): 46 | """Raised when a client is subscribed via 47 | :meth:`~tredis.RedisClient.subscribe` or 48 | :meth:`~tredis.RedisClient.psubscribe` and a command other than 49 | :meth:`~tredis.RedisClient.subscribe`, 50 | :meth:`~tredis.RedisClient.unsubscribe`, 51 | :meth:`~tredis.RedisClient.psubscribe`, or 52 | :meth:`~tredis.RedisClient.punsubscribe` was requested. Once the client 53 | enters the subscribed state it is not supposed to issue any other commands. 54 | 55 | """ 56 | pass 57 | 58 | 59 | class InvalidClusterCommand(TRedisException): 60 | """Raised when a method is invoked that is not able to be used when 61 | acting as a client for a Redis cluster. 62 | 63 | """ 64 | pass 65 | -------------------------------------------------------------------------------- /tredis/geo.py: -------------------------------------------------------------------------------- 1 | """Redis Geo Commands Mixin""" 2 | 3 | 4 | class GeoMixin(object): 5 | """Redis Geo Commands Mixin""" 6 | pass 7 | -------------------------------------------------------------------------------- /tredis/hashes.py: -------------------------------------------------------------------------------- 1 | """Redis Hash Commands Mixin""" 2 | from tornado import concurrent 3 | 4 | 5 | class HashesMixin(object): 6 | """Redis Hash Commands Mixin""" 7 | 8 | def hset(self, key, field, value): 9 | """Sets `field` in the hash stored at `key` to `value`. 10 | 11 | If `key` does not exist, a new key holding a hash is created. If 12 | `field` already exists in the hash, it is overwritten. 13 | 14 | .. note:: 15 | 16 | **Time complexity**: always ``O(1)`` 17 | 18 | :param key: The key of the hash 19 | :type key: :class:`str`, :class:`bytes` 20 | :param field: The field in the hash to set 21 | :type key: :class:`str`, :class:`bytes` 22 | :param value: The value to set the field to 23 | :returns: ``1`` if `field` is a new field in the hash and `value` 24 | was set; otherwise, ``0`` if `field` already exists in the hash 25 | and the value was updated 26 | :rtype: int 27 | 28 | """ 29 | return self._execute([b'HSET', key, field, value]) 30 | 31 | def hget(self, key, field): 32 | """ 33 | Returns the value associated with `field` in the hash stored at `key`. 34 | 35 | .. note:: 36 | 37 | **Time complexity**: always ``O(1)`` 38 | 39 | :param key: The key of the hash 40 | :type key: :class:`str`, :class:`bytes` 41 | :param field: The field in the hash to get 42 | :type key: :class:`str`, :class:`bytes` 43 | :rtype: bytes, list 44 | :raises: :exc:`~tredis.exceptions.RedisError` 45 | 46 | """ 47 | return self._execute([b'HGET', key, field]) 48 | 49 | def hgetall(self, key): 50 | """ 51 | Returns all fields and values of the has stored at `key`. 52 | 53 | The underlying redis `HGETALL`_ command returns an array of 54 | pairs. This method converts that to a Python :class:`dict`. 55 | It will return an empty :class:`dict` when the key is not 56 | found. 57 | 58 | .. note:: 59 | 60 | **Time complexity**: ``O(N)`` where ``N`` is the size 61 | of the hash. 62 | 63 | :param key: The key of the hash 64 | :type key: :class:`str`, :class:`bytes` 65 | :returns: a :class:`dict` of key to value mappings for all 66 | fields in the hash 67 | 68 | .. _HGETALL: http://redis.io/commands/hgetall 69 | 70 | """ 71 | 72 | def format_response(value): 73 | return dict(zip(value[::2], value[1::2])) 74 | 75 | return self._execute( 76 | [b'HGETALL', key], format_callback=format_response) 77 | 78 | def hmset(self, key, value_dict): 79 | """ 80 | Sets fields to values as in `value_dict` in the hash stored at `key`. 81 | 82 | Sets the specified fields to their respective values in the hash 83 | stored at `key`. This command overwrites any specified fields 84 | already existing in the hash. If `key` does not exist, a new key 85 | holding a hash is created. 86 | 87 | .. note:: 88 | 89 | **Time complexity**: ``O(N)`` where ``N`` is the number of 90 | fields being set. 91 | 92 | :param key: The key of the hash 93 | :type key: :class:`str`, :class:`bytes` 94 | :param value_dict: field to value mapping 95 | :type value_dict: :class:`dict` 96 | :rtype: bool 97 | :raises: :exc:`~tredis.exceptions.RedisError` 98 | 99 | """ 100 | if not value_dict: 101 | future = concurrent.TracebackFuture() 102 | future.set_result(False) 103 | else: 104 | command = [b'HMSET', key] 105 | command.extend(sum(value_dict.items(), ())) 106 | future = self._execute(command) 107 | return future 108 | 109 | def hmget(self, key, *fields): 110 | """ 111 | Returns the values associated with the specified `fields` in a hash. 112 | 113 | For every ``field`` that does not exist in the hash, :data:`None` 114 | is returned. Because a non-existing keys are treated as empty 115 | hashes, calling :meth:`hmget` against a non-existing key will 116 | return a list of :data:`None` values. 117 | 118 | .. note:: 119 | 120 | *Time complexity*: ``O(N)`` where ``N`` is the number of fields 121 | being requested. 122 | 123 | :param key: The key of the hash 124 | :type key: :class:`str`, :class:`bytes` 125 | :param fields: iterable of field names to retrieve 126 | :returns: a :class:`dict` of field name to value mappings for 127 | each of the requested fields 128 | :rtype: dict 129 | 130 | """ 131 | 132 | def format_response(val_array): 133 | return dict(zip(fields, val_array)) 134 | 135 | command = [b'HMGET', key] 136 | command.extend(fields) 137 | return self._execute(command, format_callback=format_response) 138 | 139 | def hdel(self, key, *fields): 140 | """ 141 | Remove the specified fields from the hash stored at `key`. 142 | 143 | Specified fields that do not exist within this hash are ignored. 144 | If `key` does not exist, it is treated as an empty hash and this 145 | command returns zero. 146 | 147 | :param key: The key of the hash 148 | :type key: :class:`str`, :class:`bytes` 149 | :param fields: iterable of field names to retrieve 150 | :returns: the number of fields that were removed from the hash, 151 | not including specified by non-existing fields. 152 | :rtype: int 153 | 154 | """ 155 | if not fields: 156 | future = concurrent.TracebackFuture() 157 | future.set_result(0) 158 | else: 159 | future = self._execute([b'HDEL', key] + list(fields)) 160 | return future 161 | 162 | def hexists(self, key, field): 163 | """ 164 | Returns if `field` is an existing field in the hash stored at `key`. 165 | 166 | .. note:: 167 | 168 | *Time complexity*: ``O(1)`` 169 | 170 | :param key: The key of the hash 171 | :type key: :class:`str`, :class:`bytes` 172 | :param field: name of the field to test for 173 | :type key: :class:`str`, :class:`bytes` 174 | :rtype: bool 175 | 176 | """ 177 | return self._execute([b'HEXISTS', key, field]) 178 | 179 | def hincrby(self, key, field, increment): 180 | """ 181 | Increments the number stored at `field` in the hash stored at `key`. 182 | 183 | If `key` does not exist, a new key holding a hash is created. If 184 | `field` does not exist the value is set to ``0`` before the operation 185 | is performed. The range of values supported is limited to 64-bit 186 | signed integers. 187 | 188 | :param key: The key of the hash 189 | :type key: :class:`str`, :class:`bytes` 190 | :param field: name of the field to increment 191 | :type key: :class:`str`, :class:`bytes` 192 | :param increment: amount to increment by 193 | :type increment: int 194 | 195 | :returns: the value at `field` after the increment occurs 196 | :rtype: int 197 | 198 | """ 199 | return self._execute( 200 | [b'HINCRBY', key, field, increment], format_callback=int) 201 | 202 | def hincrbyfloat(self, key, field, increment): 203 | """ 204 | Increments the number stored at `field` in the hash stored at `key`. 205 | 206 | If the increment value is negative, the result is to have the hash 207 | field **decremented** instead of incremented. If the field does not 208 | exist, it is set to ``0`` before performing the operation. An error 209 | is returned if one of the following conditions occur: 210 | 211 | - the field contains a value of the wrong type (not a string) 212 | - the current field content or the specified increment are not 213 | parseable as a double precision floating point number 214 | 215 | .. note:: 216 | 217 | *Time complexity*: ``O(1)`` 218 | 219 | :param key: The key of the hash 220 | :type key: :class:`str`, :class:`bytes` 221 | :param field: name of the field to increment 222 | :type key: :class:`str`, :class:`bytes` 223 | :param increment: amount to increment by 224 | :type increment: float 225 | 226 | :returns: the value at `field` after the increment occurs 227 | :rtype: float 228 | 229 | """ 230 | return self._execute( 231 | [b'HINCRBYFLOAT', key, field, increment], format_callback=float) 232 | 233 | def hkeys(self, key): 234 | """ 235 | Returns all field names in the hash stored at `key`. 236 | 237 | .. note:: 238 | 239 | *Time complexity*: ``O(N)`` where ``N`` is the size of the hash 240 | 241 | :param key: The key of the hash 242 | :type key: :class:`str`, :class:`bytes` 243 | :returns: the list of fields in the hash 244 | :rtype: list 245 | 246 | """ 247 | return self._execute([b'HKEYS', key]) 248 | 249 | def hlen(self, key): 250 | """ 251 | Returns the number of fields contained in the hash stored at `key`. 252 | 253 | .. note:: 254 | 255 | *Time complexity*: ``O(1)`` 256 | 257 | :param key: The key of the hash 258 | :type key: :class:`str`, :class:`bytes` 259 | :returns: the number of fields in the hash or zero when `key` 260 | does not exist 261 | :rtype: int 262 | 263 | """ 264 | return self._execute([b'HLEN', key]) 265 | 266 | def hsetnx(self, key, field, value): 267 | """ 268 | Sets `field` in the hash stored at `key` only if it does not exist. 269 | 270 | Sets `field` in the hash stored at `key` only if `field` does not 271 | yet exist. If `key` does not exist, a new key holding a hash is 272 | created. If `field` already exists, this operation has no effect. 273 | 274 | .. note:: 275 | 276 | *Time complexity*: ``O(1)`` 277 | 278 | :param key: The key of the hash 279 | :type key: :class:`str`, :class:`bytes` 280 | :param field: The field in the hash to set 281 | :type key: :class:`str`, :class:`bytes` 282 | :param value: The value to set the field to 283 | :returns: ``1`` if `field` is a new field in the hash and `value` 284 | was set. ``0`` if `field` already exists in the hash and 285 | no operation was performed 286 | :rtype: int 287 | 288 | """ 289 | return self._execute([b'HSETNX', key, field, value]) 290 | 291 | def hvals(self, key): 292 | """ 293 | Returns all values in the hash stored at `key`. 294 | 295 | .. note:: 296 | 297 | *Time complexity* ``O(N)`` where ``N`` is the size of the hash 298 | 299 | :param key: The key of the hash 300 | :type key: :class:`str`, :class:`bytes` 301 | :returns: a :class:`list` of :class:`bytes` instances or an 302 | empty list when `key` does not exist 303 | :rtype: list 304 | 305 | """ 306 | return self._execute([b'HVALS', key]) 307 | -------------------------------------------------------------------------------- /tredis/hyperloglog.py: -------------------------------------------------------------------------------- 1 | """Redis HyperLogLog Commands Mixin""" 2 | 3 | 4 | class HyperLogLogMixin(object): 5 | """Redis HyperLogLog Commands Mixin""" 6 | 7 | def pfadd(self, key, *elements): 8 | """Adds all the element arguments to the HyperLogLog data structure 9 | stored at the variable name specified as first argument. 10 | 11 | As a side effect of this command the HyperLogLog internals may be 12 | updated to reflect a different estimation of the number of unique items 13 | added so far (the cardinality of the set). 14 | 15 | If the approximated cardinality estimated by the HyperLogLog changed 16 | after executing the command, :meth:`~tredis.RedisClient.pfadd` returns 17 | ``1``, otherwise ``0`` is returned. The command automatically creates 18 | an empty HyperLogLog structure (that is, a Redis String of a specified 19 | length and with a given encoding) if the specified key does not exist. 20 | 21 | To call the command without elements but just the variable name is 22 | valid, this will result into no operation performed if the variable 23 | already exists, or just the creation of the data structure if the key 24 | does not exist (in the latter case ``1`` is returned). 25 | 26 | For an introduction to HyperLogLog data structure check 27 | :meth:`~tredis.RedisClient.pfcount`. 28 | 29 | .. versionadded:: 0.2.0 30 | 31 | .. note:: **Time complexity**: ``O(1)`` to add every element. 32 | 33 | :param key: The key to add the elements to 34 | :type key: :class:`str`, :class:`bytes` 35 | :param elements: One or more elements to add 36 | :type elements: :class:`str`, :class:`bytes` 37 | :rtype: bool 38 | :raises: :exc:`~tredis.exceptions.RedisError` 39 | 40 | """ 41 | return self._execute([b'PFADD', key] + list(elements), 1) 42 | 43 | def pfcount(self, *keys): 44 | """When called with a single key, returns the approximated cardinality 45 | computed by the HyperLogLog data structure stored at the specified 46 | variable, which is ``0`` if the variable does not exist. 47 | 48 | When called with multiple keys, returns the approximated cardinality of 49 | the union of the HyperLogLogs passed, by internally merging the 50 | HyperLogLogs stored at the provided keys into a temporary HyperLogLog. 51 | 52 | The HyperLogLog data structure can be used in order to count unique 53 | elements in a set using just a small constant amount of memory, 54 | specifically 12k bytes for every HyperLogLog (plus a few bytes for the 55 | key itself). 56 | 57 | The returned cardinality of the observed set is not exact, but 58 | approximated with a standard error of 0.81%. 59 | 60 | For example in order to take the count of all the unique search queries 61 | performed in a day, a program needs to call 62 | :meth:`~tredis.RedisCount.pfcount` every time a query is processed. The 63 | estimated number of unique queries can be retrieved with 64 | :meth:`~tredis.RedisCount.pfcount` at any time. 65 | 66 | .. note:: as a side effect of calling this function, it is possible 67 | that the HyperLogLog is modified, since the last 8 bytes encode the 68 | latest computed cardinality for caching purposes. So 69 | :meth:`~tredis.RedisCount.pfcount` is technically a write command. 70 | 71 | .. versionadded:: 0.2.0 72 | 73 | .. note:: **Time complexity**: ``O(1)`` with every small average 74 | constant times when called with a single key. ``O(N)`` with ``N`` 75 | being the number of keys, and much bigger constant times, when 76 | called with multiple keys. 77 | 78 | :param keys: One or more keys 79 | :type keys: :class:`str`, :class:`bytes` 80 | :rtype: int 81 | :returns: The approximated number of unique elements observed 82 | :raises: :exc:`~tredis.exceptions.RedisError` 83 | 84 | """ 85 | return self._execute([b'PFCOUNT'] + list(keys)) 86 | 87 | def pfmerge(self, dest_key, *keys): 88 | """Merge multiple HyperLogLog values into an unique value that will 89 | approximate the cardinality of the union of the observed Sets of the 90 | source HyperLogLog structures. 91 | 92 | The computed merged HyperLogLog is set to the destination variable, 93 | which is created if does not exist (defaulting to an empty 94 | HyperLogLog). 95 | 96 | .. versionadded:: 0.2.0 97 | 98 | .. note:: 99 | 100 | **Time complexity**: ``O(N)`` to merge ``N`` HyperLogLogs, but 101 | with high constant times. 102 | 103 | :param dest_key: The destination key 104 | :type dest_key: :class:`str`, :class:`bytes` 105 | :param keys: One or more keys 106 | :type keys: :class:`str`, :class:`bytes` 107 | :rtype: bool 108 | :raises: :exc:`~tredis.exceptions.RedisError` 109 | 110 | """ 111 | return self._execute([b'PFMERGE', dest_key] + list(keys), b'OK') 112 | -------------------------------------------------------------------------------- /tredis/lists.py: -------------------------------------------------------------------------------- 1 | """Redis List Commands Mixin""" 2 | 3 | 4 | class ListsMixin(object): 5 | """Redis List Commands Mixin""" 6 | 7 | def llen(self, key): 8 | """ 9 | Returns the length of the list stored at key. 10 | 11 | :param key: The list's key 12 | :type key: :class:`str`, :class:`bytes` 13 | :rtype: int 14 | :raises: :exc:`~tredis.exceptions.TRedisException` 15 | 16 | If key does not exist, it is interpreted as an empty list and 0 is 17 | returned. An error is returned when the value stored at key is not a 18 | list. 19 | 20 | .. note:: 21 | 22 | **Time complexity** ``O(1)`` 23 | 24 | """ 25 | return self._execute([b'LLEN', key]) 26 | 27 | def lrange(self, key, start, end): 28 | """ 29 | Returns the specified elements of the list stored at key. 30 | 31 | :param key: The list's key 32 | :type key: :class:`str`, :class:`bytes` 33 | :param int start: zero-based index to start retrieving elements from 34 | :param int end: zero-based index at which to stop retrieving elements 35 | 36 | :rtype: list 37 | :raises: :exc:`~tredis.exceptions.TRedisException` 38 | 39 | The offsets start and stop are zero-based indexes, with 0 being the 40 | first element of the list (the head of the list), 1 being the next 41 | element and so on. 42 | 43 | These offsets can also be negative numbers indicating offsets 44 | starting at the end of the list. For example, -1 is the last element 45 | of the list, -2 the penultimate, and so on. 46 | 47 | Note that if you have a list of numbers from 0 to 100, 48 | ``lrange(key, 0, 10)`` will return 11 elements, that is, the 49 | rightmost item is included. This may or may not be consistent with 50 | behavior of range-related functions in your programming language of 51 | choice (think Ruby's ``Range.new``, ``Array#slice`` or Python's 52 | :func:`range` function). 53 | 54 | Out of range indexes will not produce an error. If start is larger 55 | than the end of the list, an empty list is returned. If stop is 56 | larger than the actual end of the list, Redis will treat it like the 57 | last element of the list. 58 | 59 | .. note:: 60 | 61 | **Time complexity** ``O(S+N)`` where ``S`` is the distance of 62 | start offset from ``HEAD`` for small lists, from nearest end 63 | (``HEAD`` or ``TAIL``) for large lists; and ``N`` is the number 64 | of elements in the specified range. 65 | 66 | """ 67 | return self._execute([b'LRANGE', key, start, end]) 68 | 69 | def ltrim(self, key, start, stop): 70 | """ 71 | Crop a list to the specified range. 72 | 73 | :param key: The list's key 74 | :type key: :class:`str`, :class:`bytes` 75 | :param int start: zero-based index to first element to retain 76 | :param int stop: zero-based index of the last element to retain 77 | :returns: did the operation succeed? 78 | :rtype: bool 79 | :raises: :exc:`~tredis.exceptions.TRedisException` 80 | 81 | Trim an existing list so that it will contain only the specified 82 | range of elements specified. 83 | 84 | Both `start` and `stop` are zero-based indexes, where 0 is the first 85 | element of the list (the head), 1 the next element and so on. 86 | For example: ``ltrim('foobar', 0, 2)`` will modify the list stored at 87 | ``foobar`` so that only the first three elements of the list will 88 | remain. 89 | 90 | `start` and `stop` can also be negative numbers indicating offsets 91 | from the end of the list, where -1 is the last element of the list, 92 | -2 the penultimate element and so on. 93 | 94 | Out of range indexes will not produce an error: if `start` is larger 95 | than the `end` of the list, or `start > end`, the result will be an 96 | empty list (which causes `key` to be removed). If `end` is larger 97 | than the end of the list, Redis will treat it like the last element 98 | of the list. 99 | 100 | A common use of LTRIM is together with LPUSH / RPUSH. For example:: 101 | 102 | client.lpush('mylist', 'somelement') 103 | client.ltrim('mylist', 0, 99) 104 | 105 | This pair of commands will push a new element on the list, while 106 | making sure that the list will not grow larger than 100 elements. 107 | This is very useful when using Redis to store logs for example. It is 108 | important to note that when used in this way LTRIM is an O(1) 109 | operation because in the average case just one element is removed 110 | from the tail of the list. 111 | 112 | .. note:: 113 | 114 | Time complexity: ``O(N)`` where `N` is the number of elements to 115 | be removed by the operation. 116 | 117 | """ 118 | return self._execute([b'LTRIM', key, start, stop], b'OK') 119 | 120 | def lpush(self, key, *values): 121 | """ 122 | Insert all the specified values at the head of the list stored at key. 123 | 124 | :param key: The list's key 125 | :type key: :class:`str`, :class:`bytes` 126 | :param values: One or more positional arguments to insert at the 127 | beginning of the list. Each value is inserted at the beginning 128 | of the list individually (see discussion below). 129 | :returns: the length of the list after push operations 130 | :rtype: int 131 | :raises: :exc:`~tredis.exceptions.TRedisException` 132 | 133 | If `key` does not exist, it is created as empty list before 134 | performing the push operations. When key holds a value that is not a 135 | list, an error is returned. 136 | 137 | It is possible to push multiple elements using a single command call 138 | just specifying multiple arguments at the end of the command. 139 | Elements are inserted one after the other to the head of the list, 140 | from the leftmost element to the rightmost element. So for instance 141 | ``client.lpush('mylist', 'a', 'b', 'c')`` will result into a list 142 | containing ``c`` as first element, ``b`` as second element and ``a`` 143 | as third element. 144 | 145 | .. note:: 146 | 147 | **Time complexity**: ``O(1)`` 148 | 149 | """ 150 | return self._execute([b'LPUSH', key] + list(values)) 151 | 152 | def lpushx(self, key, *values): 153 | """ 154 | Insert values at the head of an existing list. 155 | 156 | :param key: The list's key 157 | :type key: :class:`str`, :class:`bytes` 158 | :param values: One or more positional arguments to insert at the 159 | beginning of the list. Each value is inserted at the beginning 160 | of the list individually (see discussion below). 161 | :returns: the length of the list after push operations, zero if 162 | `key` does not refer to a list 163 | :rtype: int 164 | :raises: :exc:`~tredis.exceptions.TRedisException` 165 | 166 | This method inserts `values` at the head of the list stored at `key`, 167 | only if `key` already exists and holds a list. In contrary to 168 | :meth:`.lpush`, no operation will be performed when key does not yet 169 | exist. 170 | 171 | .. note:: 172 | 173 | **Time complexity**: ``O(1)`` 174 | 175 | """ 176 | return self._execute([b'LPUSHX', key] + list(values)) 177 | 178 | def lpop(self, key): 179 | """ 180 | Removes and returns the first element of the list stored at key. 181 | 182 | :param key: The list's key 183 | :type key: :class:`str`, :class:`bytes` 184 | :returns: the element at the head of the list, :data:`None` if the 185 | list does not exist 186 | :raises: :exc:`~tredis.exceptions.TRedisException` 187 | 188 | .. note:: 189 | 190 | **Time complexity**: ``O(1)`` 191 | 192 | """ 193 | return self._execute([b'LPOP', key]) 194 | 195 | def rpush(self, key, *values): 196 | """ 197 | Insert all the specified values at the tail of the list stored at key. 198 | 199 | :param key: The list's key 200 | :type key: :class:`str`, :class:`bytes` 201 | :param values: One or more positional arguments to insert at the 202 | tail of the list. 203 | :returns: the length of the list after push operations 204 | :rtype: int 205 | :raises: :exc:`~tredis.exceptions.TRedisException` 206 | 207 | If `key` does not exist, it is created as empty list before performing 208 | the push operation. When `key` holds a value that is not a list, an 209 | error is returned. 210 | 211 | It is possible to push multiple elements using a single command call 212 | just specifying multiple arguments at the end of the command. 213 | Elements are inserted one after the other to the tail of the list, 214 | from the leftmost element to the rightmost element. So for instance 215 | the command ``client.rpush('mylist', 'a', 'b', 'c')`` will result 216 | in a list containing ``a`` as first element, ``b`` as second element 217 | and ``c`` as third element. 218 | 219 | .. note:: 220 | 221 | **Time complexity**: ``O(1)`` 222 | 223 | """ 224 | return self._execute([b'RPUSH', key] + list(values)) 225 | 226 | def rpushx(self, key, *values): 227 | """ 228 | Insert values at the tail of an existing list. 229 | 230 | :param key: The list's key 231 | :type key: :class:`str`, :class:`bytes` 232 | :param values: One or more positional arguments to insert at the 233 | tail of the list. 234 | :returns: the length of the list after push operations or 235 | zero if `key` does not refer to a list 236 | :rtype: int 237 | :raises: :exc:`~tredis.exceptions.TRedisException` 238 | 239 | This method inserts value at the tail of the list stored at `key`, 240 | only if `key` already exists and holds a list. In contrary to 241 | method:`.rpush`, no operation will be performed when `key` does not 242 | yet exist. 243 | 244 | .. note:: 245 | 246 | **Time complexity**: ``O(1)`` 247 | 248 | """ 249 | return self._execute([b'RPUSHX', key] + list(values)) 250 | 251 | def rpop(self, key): 252 | """ 253 | Removes and returns the last element of the list stored at key. 254 | 255 | :param key: The list's key 256 | :type key: :class:`str`, :class:`bytes` 257 | :returns: the length of the list after push operations or 258 | zero if `key` does not refer to a list 259 | :returns: the element at the tail of the list, :data:`None` if the 260 | list does not exist 261 | :rtype: int 262 | :raises: :exc:`~tredis.exceptions.TRedisException` 263 | 264 | """ 265 | return self._execute([b'RPOP', key]) 266 | -------------------------------------------------------------------------------- /tredis/pubsub.py: -------------------------------------------------------------------------------- 1 | """Redis PubSub Commands Mixin""" 2 | 3 | 4 | class PubSubMixin(object): 5 | """Redis PubSub Commands Mixin""" 6 | pass 7 | -------------------------------------------------------------------------------- /tredis/scripting.py: -------------------------------------------------------------------------------- 1 | """Redis Scripting Commands Mixin""" 2 | 3 | 4 | class ScriptingMixin(object): 5 | """Redis Scripting Commands Mixin""" 6 | 7 | def eval(self, script, keys=None, args=None): 8 | """:meth:`~tredis.RedisClient.eval` and 9 | :meth:`~tredis.RedisClient.evalsha` are used to evaluate scripts using 10 | the Lua interpreter built into Redis starting from version 2.6.0. 11 | 12 | The first argument of EVAL is a Lua 5.1 script. The script does not 13 | need to define a Lua function (and should not). It is just a Lua 14 | program that will run in the context of the Redis server. 15 | 16 | .. note:: 17 | 18 | **Time complexity**: Depends on the script that is executed. 19 | 20 | :param str script: The Lua script to execute 21 | :param list keys: A list of keys to pass into the script 22 | :param list args: A list of args to pass into the script 23 | :return: mixed 24 | 25 | """ 26 | if not keys: 27 | keys = [] 28 | if not args: 29 | args = [] 30 | return self._execute([b'EVAL', script, str(len(keys))] + keys + args) 31 | 32 | def evalsha(self, sha1, keys=None, args=None): 33 | """Evaluates a script cached on the server side by its SHA1 digest. 34 | Scripts are cached on the server side using the 35 | :meth:`~tredis.RedisClient.script_load` command. The command is 36 | otherwise identical to :meth:`~tredis.RedisClient.eval`. 37 | 38 | .. note:: 39 | 40 | **Time complexity**: Depends on the script that is executed. 41 | 42 | :param str sha1: The sha1 hash of the script to execute 43 | :param list keys: A list of keys to pass into the script 44 | :param list args: A list of args to pass into the script 45 | :return: mixed 46 | 47 | """ 48 | if not keys: 49 | keys = [] 50 | if not args: 51 | args = [] 52 | return self._execute([b'EVALSHA', sha1, str(len(keys))] + keys + args) 53 | 54 | def script_exists(self, *hashes): 55 | """Returns information about the existence of the scripts in the script 56 | cache. 57 | 58 | This command accepts one or more SHA1 digests and returns a list of 59 | ones or zeros to signal if the scripts are already defined or not 60 | inside the script cache. This can be useful before a pipelining 61 | operation to ensure that scripts are loaded (and if not, to load them 62 | using :meth:`~tredis.RedisClient.script_load`) so that the pipelining 63 | operation can be performed solely using 64 | :meth:`~tredis.RedisClient.evalsha` instead of 65 | :meth:`~tredis.RedisClient.eval` to save bandwidth. 66 | 67 | Please refer to the :meth:`~tredis.RedisClient.eval` documentation for 68 | detailed information about Redis Lua scripting. 69 | 70 | .. note:: 71 | 72 | **Time complexity**: ``O(N)`` with ``N`` being the number of scripts 73 | to check (so checking a single script is an ``O(1)`` operation). 74 | 75 | :param str hashes: One or more sha1 hashes to check for in the cache 76 | :rtype: list 77 | :return: Returns a list of ``1`` or ``0`` indicating if the specified 78 | script(s) exist in the cache. 79 | 80 | """ 81 | return self._execute([b'SCRIPT', b'EXISTS'] + list(hashes)) 82 | 83 | def script_flush(self): 84 | """Flush the Lua scripts cache. 85 | 86 | Please refer to the :meth:`~tredis.RedisClient.eval` documentation for 87 | detailed information about Redis Lua scripting. 88 | 89 | .. note:: 90 | 91 | **Time complexity**: ``O(N)`` with ``N`` being the number of scripts 92 | in cache 93 | 94 | :rtype: bool 95 | 96 | """ 97 | return self._execute([b'SCRIPT', b'FLUSH'], b'OK') 98 | 99 | def script_kill(self): 100 | """Kills the currently executing Lua script, assuming no write 101 | operation was yet performed by the script. 102 | 103 | This command is mainly useful to kill a script that is running for too 104 | much time(for instance because it entered an infinite loop because of 105 | a bug). The script will be killed and the client currently blocked into 106 | :meth:`~tredis.RedisClient.eval` will see the command returning with an 107 | error. 108 | 109 | If the script already performed write operations it can not be killed 110 | in this way because it would violate Lua script atomicity contract. In 111 | such a case only SHUTDOWN NOSAVE is able to kill the script, killing 112 | the Redis process in an hard way preventing it to persist with 113 | half-written information. 114 | 115 | Please refer to the :meth:`~tredis.RedisClient.eval` documentation for 116 | detailed information about Redis Lua scripting. 117 | 118 | .. note:: 119 | 120 | **Time complexity**: ``O(1)`` 121 | 122 | :rtype: bool 123 | 124 | """ 125 | return self._execute([b'SCRIPT', b'KILL'], b'OK') 126 | 127 | def script_load(self, script): 128 | """Load a script into the scripts cache, without executing it. After 129 | the specified command is loaded into the script cache it will be 130 | callable using :meth:`~tredis.RedisClient.evalsha` with the correct 131 | SHA1 digest of the script, exactly like after the first successful 132 | invocation of :meth:`~tredis.RedisClient.eval`. 133 | 134 | The script is guaranteed to stay in the script cache forever (unless 135 | :meth:`~tredis.RedisClient.script_flush` is called). 136 | 137 | The command works in the same way even if the script was already 138 | present in the script cache. 139 | 140 | Please refer to the :meth:`~tredis.RedisClient.eval` documentation for 141 | detailed information about Redis Lua scripting. 142 | 143 | .. note:: 144 | 145 | **Time complexity**: ``O(N)`` with ``N`` being the length in bytes 146 | of the script body. 147 | 148 | :param str script: The script to load into the script cache 149 | :return: str 150 | 151 | """ 152 | return self._execute([b'SCRIPT', b'LOAD', script]) 153 | -------------------------------------------------------------------------------- /tredis/server.py: -------------------------------------------------------------------------------- 1 | """Redis Server Commands Mixin""" 2 | from tornado import concurrent 3 | 4 | from tredis import common, exceptions 5 | 6 | # Python 2 support for ascii() 7 | if 'ascii' not in dir(__builtins__): # pragma: nocover 8 | from tredis.compat import ascii 9 | 10 | 11 | class ServerMixin(object): 12 | """Redis Server Commands Mixin""" 13 | 14 | def auth(self, password): 15 | """Request for authentication in a password-protected Redis server. 16 | Redis can be instructed to require a password before allowing clients 17 | to execute commands. This is done using the ``requirepass`` directive 18 | in the configuration file. 19 | 20 | If the password does not match, an 21 | :exc:`~tredis.exceptions.AuthError` exception 22 | will be raised. 23 | 24 | :param password: The password to authenticate with 25 | :type password: :class:`str`, :class:`bytes` 26 | :rtype: bool 27 | :raises: :exc:`~tredis.exceptions.AuthError`, 28 | :exc:`~tredis.exceptions.RedisError` 29 | 30 | """ 31 | future = concurrent.TracebackFuture() 32 | 33 | def on_response(response): 34 | """Process the redis response 35 | 36 | :param response: The future with the response 37 | :type response: tornado.concurrent.Future 38 | 39 | """ 40 | exc = response.exception() 41 | if exc: 42 | if exc.args[0] == b'invalid password': 43 | future.set_exception(exceptions.AuthError(exc)) 44 | else: 45 | future.set_exception(exc) 46 | else: 47 | future.set_result(response.result()) 48 | 49 | execute_future = self._execute([b'AUTH', password], b'OK') 50 | self.io_loop.add_future(execute_future, on_response) 51 | return future 52 | 53 | def echo(self, message): 54 | """Returns the message that was sent to the Redis server. 55 | 56 | :param message: The message to echo 57 | :type message: :class:`str`, :class:`bytes` 58 | :rtype: bytes 59 | :raises: :exc:`~tredis.exceptions.RedisError` 60 | 61 | """ 62 | return self._execute([b'ECHO', message]) 63 | 64 | def info(self, section=None): 65 | """The INFO command returns information and statistics about the server 66 | in a format that is simple to parse by computers and easy to read by 67 | humans. 68 | 69 | The optional parameter can be used to select a specific section of 70 | information: 71 | 72 | - server: General information about the Redis server 73 | - clients: Client connections section 74 | - memory: Memory consumption related information 75 | - persistence: RDB and AOF related information 76 | - stats: General statistics 77 | - replication: Master/slave replication information 78 | - cpu: CPU consumption statistics 79 | - commandstats: Redis command statistics 80 | - cluster: Redis Cluster section 81 | - keyspace: Database related statistics 82 | 83 | It can also take the following values: 84 | 85 | - all: Return all sections 86 | - default: Return only the default set of sections 87 | 88 | When no parameter is provided, the default option is assumed. 89 | 90 | :param str section: Optional 91 | :return: dict 92 | 93 | """ 94 | cmd = [b'INFO'] 95 | if section: 96 | cmd.append(section) 97 | return self._execute(cmd, format_callback=common.format_info_response) 98 | 99 | def ping(self): 100 | """Returns ``PONG`` if no argument is provided, otherwise return a copy 101 | of the argument as a bulk. This command is often used to test if a 102 | connection is still alive, or to measure latency. 103 | 104 | If the client is subscribed to a channel or a pattern, it will instead 105 | return a multi-bulk with a ``pong`` in the first position and an empty 106 | bulk in the second position, unless an argument is provided in which 107 | case it returns a copy of the argument. 108 | 109 | :rtype: bytes 110 | :raises: :exc:`~tredis.exceptions.RedisError` 111 | 112 | """ 113 | return self._execute([b'PING']) 114 | 115 | def quit(self): 116 | """Ask the server to close the connection. The connection is closed as 117 | soon as all pending replies have been written to the client. 118 | 119 | :rtype: bool 120 | :raises: :exc:`~tredis.exceptions.RedisError` 121 | 122 | """ 123 | self._closing = True 124 | return self._execute([b'QUIT'], b'OK') 125 | 126 | def select(self, index=0): 127 | """Select the DB with having the specified zero-based numeric index. 128 | New connections always use DB ``0``. 129 | 130 | :param int index: The database to select 131 | :rtype: bool 132 | :raises: :exc:`~tredis.exceptions.RedisError` 133 | :raises: :exc:`~tredis.exceptions.InvalidClusterCommand` 134 | 135 | """ 136 | if self._clustering: 137 | raise exceptions.InvalidClusterCommand 138 | future = self._execute( 139 | [b'SELECT', ascii(index).encode('ascii')], b'OK') 140 | 141 | def on_selected(f): 142 | self._connection.database = index 143 | 144 | self.io_loop.add_future(future, on_selected) 145 | return future 146 | 147 | def time(self): 148 | """Retrieve the current time from the redis server. 149 | 150 | :rtype: float 151 | :raises: :exc:`~tredis.exceptions.RedisError` 152 | 153 | """ 154 | 155 | def format_response(value): 156 | """Format a TIME response into a datetime.datetime 157 | 158 | :param list value: TIME response is a list of the number 159 | of seconds since the epoch and the number of micros 160 | as two byte strings 161 | :rtype: float 162 | 163 | """ 164 | seconds, micros = value 165 | return float(seconds) + (float(micros) / 1000000.0) 166 | 167 | return self._execute([b'TIME'], format_callback=format_response) 168 | -------------------------------------------------------------------------------- /tredis/sets.py: -------------------------------------------------------------------------------- 1 | """Redis Set Commands Mixin""" 2 | from tornado import concurrent 3 | 4 | # Python 2 support for ascii() 5 | if 'ascii' not in dir(__builtins__): # pragma: nocover 6 | from tredis.compat import ascii 7 | 8 | 9 | class SetsMixin(object): 10 | """Redis Set Commands Mixin""" 11 | 12 | def sadd(self, key, *members): 13 | """Add the specified members to the set stored at key. Specified 14 | members that are already a member of this set are ignored. If key does 15 | not exist, a new set is created before adding the specified members. 16 | 17 | An error is returned when the value stored at key is not a set. 18 | 19 | Returns :data:`True` if all requested members are added. If more 20 | than one member is passed in and not all members are added, the 21 | number of added members is returned. 22 | 23 | .. note:: 24 | 25 | **Time complexity**: ``O(N)`` where ``N`` is the number of members 26 | to be added. 27 | 28 | :param key: The key of the set 29 | :type key: :class:`str`, :class:`bytes` 30 | :param members: One or more positional arguments to add to the set 31 | :type key: :class:`str`, :class:`bytes` 32 | :returns: Number of items added to the set 33 | :rtype: bool, int 34 | 35 | """ 36 | return self._execute([b'SADD', key] + list(members), len(members)) 37 | 38 | def scard(self, key): 39 | """Returns the set cardinality (number of elements) of the set stored 40 | at key. 41 | 42 | .. note:: 43 | 44 | **Time complexity**: ``O(1)`` 45 | 46 | :param key: The key of the set 47 | :type key: :class:`str`, :class:`bytes` 48 | :rtype: int 49 | :raises: :exc:`~tredis.exceptions.RedisError` 50 | 51 | """ 52 | return self._execute([b'SCARD', key]) 53 | 54 | def sdiff(self, *keys): 55 | """Returns the members of the set resulting from the difference between 56 | the first set and all the successive sets. 57 | 58 | For example: 59 | 60 | .. code:: 61 | 62 | key1 = {a,b,c,d} 63 | key2 = {c} 64 | key3 = {a,c,e} 65 | SDIFF key1 key2 key3 = {b,d} 66 | 67 | Keys that do not exist are considered to be empty sets. 68 | 69 | .. note:: 70 | 71 | **Time complexity**: ``O(N)`` where ``N`` is the total number of 72 | elements in all given sets. 73 | 74 | :param keys: Two or more set keys as positional arguments 75 | :type keys: :class:`str`, :class:`bytes` 76 | :rtype: list 77 | :raises: :exc:`~tredis.exceptions.RedisError` 78 | 79 | """ 80 | return self._execute([b'SDIFF'] + list(keys)) 81 | 82 | def sdiffstore(self, destination, *keys): 83 | """This command is equal to :meth:`~tredis.RedisClient.sdiff`, but 84 | instead of returning the resulting set, it is stored in destination. 85 | 86 | If destination already exists, it is overwritten. 87 | 88 | .. note:: 89 | 90 | **Time complexity**: ``O(N)`` where ``N`` is the total number of 91 | elements in all given sets. 92 | 93 | :param destination: The set to store the diff into 94 | :type destination: :class:`str`, :class:`bytes` 95 | :param keys: One or more set keys as positional arguments 96 | :type keys: :class:`str`, :class:`bytes` 97 | :rtype: int 98 | :raises: :exc:`~tredis.exceptions.RedisError` 99 | 100 | """ 101 | return self._execute([b'SDIFFSTORE', destination] + list(keys)) 102 | 103 | def sinter(self, *keys): 104 | """Returns the members of the set resulting from the intersection of 105 | all the given sets. 106 | 107 | For example: 108 | 109 | .. code:: 110 | 111 | key1 = {a,b,c,d} 112 | key2 = {c} 113 | key3 = {a,c,e} 114 | SINTER key1 key2 key3 = {c} 115 | 116 | Keys that do not exist are considered to be empty sets. With one of 117 | the keys being an empty set, the resulting set is also empty (since 118 | set intersection with an empty set always results in an empty set). 119 | 120 | .. note:: 121 | 122 | **Time complexity**: ``O(N*M)`` worst case where ``N`` is the 123 | cardinality of the smallest set and ``M`` is the number of sets. 124 | 125 | :param keys: Two or more set keys as positional arguments 126 | :type keys: :class:`str`, :class:`bytes` 127 | :rtype: list 128 | :raises: :exc:`~tredis.exceptions.RedisError` 129 | 130 | """ 131 | return self._execute([b'SINTER'] + list(keys)) 132 | 133 | def sinterstore(self, destination, *keys): 134 | """This command is equal to :meth:`~tredis.RedisClient.sinter`, but 135 | instead of returning the resulting set, it is stored in destination. 136 | 137 | If destination already exists, it is overwritten. 138 | 139 | .. note:: 140 | 141 | **Time complexity**: ``O(N*M)`` worst case where ``N`` is the 142 | cardinality of the smallest set and ``M`` is the number of sets. 143 | 144 | :param destination: The set to store the intersection into 145 | :type destination: :class:`str`, :class:`bytes` 146 | :param keys: One or more set keys as positional arguments 147 | :type keys: :class:`str`, :class:`bytes` 148 | :rtype: int 149 | :raises: :exc:`~tredis.exceptions.RedisError` 150 | 151 | """ 152 | return self._execute([b'SINTERSTORE', destination] + list(keys)) 153 | 154 | def sismember(self, key, member): 155 | """Returns :data:`True` if ``member`` is a member of the set stored 156 | at key. 157 | 158 | .. note:: 159 | 160 | **Time complexity**: ``O(1)`` 161 | 162 | :param key: The key of the set to check for membership in 163 | :type key: :class:`str`, :class:`bytes` 164 | :param member: The value to check for set membership with 165 | :type member: :class:`str`, :class:`bytes` 166 | :rtype: bool 167 | :raises: :exc:`~tredis.exceptions.RedisError` 168 | 169 | """ 170 | return self._execute([b'SISMEMBER', key, member], 1) 171 | 172 | def smembers(self, key): 173 | """Returns all the members of the set value stored at key. 174 | 175 | This has the same effect as running :meth:`~tredis.RedisClient.sinter` 176 | with one argument key. 177 | 178 | .. note:: 179 | 180 | **Time complexity**: ``O(N)`` where ``N`` is the set cardinality. 181 | 182 | :param key: The key of the set to return the members from 183 | :type key: :class:`str`, :class:`bytes` 184 | :rtype: list 185 | :raises: :exc:`~tredis.exceptions.RedisError` 186 | 187 | """ 188 | return self._execute([b'SMEMBERS', key]) 189 | 190 | def smove(self, source, destination, member): 191 | """Move member from the set at source to the set at destination. This 192 | operation is atomic. In every given moment the element will appear to 193 | be a member of source or destination for other clients. 194 | 195 | If the source set does not exist or does not contain the specified 196 | element, no operation is performed and :data:`False` is returned. 197 | Otherwise, the element is removed from the source set and added to the 198 | destination set. When the specified element already exists in the 199 | destination set, it is only removed from the source set. 200 | 201 | An error is returned if source or destination does not hold a set 202 | value. 203 | 204 | .. note:: 205 | 206 | **Time complexity**: ``O(1)`` 207 | 208 | :param source: The source set key 209 | :type source: :class:`str`, :class:`bytes` 210 | :param destination: The destination set key 211 | :type destination: :class:`str`, :class:`bytes` 212 | :param member: The member value to move 213 | :type member: :class:`str`, :class:`bytes` 214 | :rtype: bool 215 | :raises: :exc:`~tredis.exceptions.RedisError` 216 | 217 | """ 218 | return self._execute([b'SMOVE', source, destination, member], 1) 219 | 220 | def spop(self, key, count=None): 221 | """Removes and returns one or more random elements from the set value 222 | store at key. 223 | 224 | This operation is similar to :meth:`~tredis.RedisClient.srandmember`, 225 | that returns one or more random elements from a set but does not remove 226 | it. 227 | 228 | The count argument will be available in a later version and is not 229 | available in 2.6, 2.8, 3.0 230 | 231 | Redis 3.2 will be the first version where an optional count argument 232 | can be passed to :meth:`~tredis.RedisClient.spop` in order to retrieve 233 | multiple elements in a single call. The implementation is already 234 | available in the unstable branch. 235 | 236 | .. note:: 237 | 238 | **Time complexity**: Without the count argument ``O(1)``, otherwise 239 | ``O(N)`` where ``N`` is the absolute value of the passed count. 240 | 241 | :param key: The key to get one or more random members from 242 | :type key: :class:`str`, :class:`bytes` 243 | :param int count: The number of members to return 244 | :rtype: bytes, list 245 | :raises: :exc:`~tredis.exceptions.RedisError` 246 | 247 | """ 248 | command = [b'SPOP', key] 249 | if count: # pragma: nocover 250 | command.append(ascii(count).encode('ascii')) 251 | return self._execute(command) 252 | 253 | def srandmember(self, key, count=None): 254 | """When called with just the key argument, return a random element from 255 | the set value stored at key. 256 | 257 | Starting from Redis version 2.6, when called with the additional count 258 | argument, return an array of count distinct elements if count is 259 | positive. If called with a negative count the behavior changes and the 260 | command is allowed to return the same element multiple times. In this 261 | case the number of returned elements is the absolute value of the 262 | specified count. 263 | 264 | When called with just the key argument, the operation is similar to 265 | :meth:`~tredis.RedisClient.spop`, however while 266 | :meth:`~tredis.RedisClient.spop` also removes the randomly selected 267 | element from the set, :meth:`~tredis.RedisClient.srandmember` will just 268 | return a random element without altering the original set in any way. 269 | 270 | .. note:: 271 | 272 | **Time complexity**: Without the count argument ``O(1)``, otherwise 273 | ``O(N)`` where ``N`` is the absolute value of the passed count. 274 | 275 | :param key: The key to get one or more random members from 276 | :type key: :class:`str`, :class:`bytes` 277 | :param int count: The number of members to return 278 | :rtype: bytes, list 279 | :raises: :exc:`~tredis.exceptions.RedisError` 280 | 281 | """ 282 | command = [b'SRANDMEMBER', key] 283 | if count: 284 | command.append(ascii(count).encode('ascii')) 285 | return self._execute(command) 286 | 287 | def srem(self, key, *members): 288 | """Remove the specified members from the set stored at key. Specified 289 | members that are not a member of this set are ignored. If key does not 290 | exist, it is treated as an empty set and this command returns ``0``. 291 | 292 | An error is returned when the value stored at key is not a set. 293 | 294 | Returns :data:`True` if all requested members are removed. If more 295 | than one member is passed in and not all members are removed, the 296 | number of removed members is returned. 297 | 298 | .. note:: 299 | 300 | **Time complexity**: ``O(N)`` where ``N`` is the number of members 301 | to be removed. 302 | 303 | :param key: The key to remove the member from 304 | :type key: :class:`str`, :class:`bytes` 305 | :param mixed members: One or more member values to remove 306 | :rtype: bool, int 307 | :raises: :exc:`~tredis.exceptions.RedisError` 308 | 309 | """ 310 | return self._execute([b'SREM', key] + list(members), len(members)) 311 | 312 | def sscan(self, key, cursor=0, pattern=None, count=None): 313 | """The :meth:`~tredis.RedisClient.sscan` command and the closely 314 | related commands :meth:`~tredis.RedisClient.scan`, 315 | :meth:`~tredis.RedisClient.hscan` and :meth:`~tredis.RedisClient.zscan` 316 | are used in order to incrementally iterate over a collection of 317 | elements. 318 | 319 | - :meth:`~tredis.RedisClient.scan` iterates the set of keys in the 320 | currently selected Redis database. 321 | - :meth:`~tredis.RedisClient.sscan` iterates elements of Sets types. 322 | - :meth:`~tredis.RedisClient.hscan` iterates fields of Hash types and 323 | their associated values. 324 | - :meth:`~tredis.RedisClient.zscan` iterates elements of Sorted Set 325 | types and their associated scores. 326 | 327 | **Basic usage** 328 | 329 | :meth:`~tredis.RedisClient.sscan` is a cursor based iterator. This 330 | means that at every call of the command, the server returns an updated 331 | cursor that the user needs to use as the cursor argument in the next 332 | call. 333 | 334 | An iteration starts when the cursor is set to ``0``, and terminates 335 | when the cursor returned by the server is ``0``. 336 | 337 | For more information on :meth:`~tredis.RedisClient.scan`, 338 | visit the `Redis docs on scan `_. 339 | 340 | .. note:: 341 | 342 | **Time complexity**: ``O(1)`` for every call. ``O(N)`` for a 343 | complete iteration, including enough command calls for the cursor to 344 | return back to ``0``. ``N`` is the number of elements inside the 345 | collection. 346 | 347 | :param key: The key to scan 348 | :type key: :class:`str`, :class:`bytes` 349 | :param int cursor: The server specified cursor value or ``0`` 350 | :param pattern: An optional pattern to apply for key matching 351 | :type pattern: :class:`str`, :class:`bytes` 352 | :param int count: An optional amount of work to perform in the scan 353 | :rtype: int, list 354 | :returns: A tuple containing the cursor and the list of set items 355 | :raises: :exc:`~tredis.exceptions.RedisError` 356 | 357 | """ 358 | 359 | def format_response(value): 360 | """Format the response from redis 361 | 362 | :param tuple value: The return response from redis 363 | :rtype: tuple(int, list) 364 | 365 | """ 366 | return int(value[0]), value[1] 367 | 368 | command = [b'SSCAN', key, ascii(cursor).encode('ascii')] 369 | if pattern: 370 | command += [b'MATCH', pattern] 371 | if count: 372 | command += [b'COUNT', ascii(count).encode('ascii')] 373 | return self._execute(command, format_callback=format_response) 374 | 375 | def sunion(self, *keys): 376 | """Returns the members of the set resulting from the union of all the 377 | given sets. 378 | 379 | For example: 380 | 381 | .. code:: 382 | 383 | key1 = {a,b,c,d} 384 | key2 = {c} 385 | key3 = {a,c,e} 386 | SUNION key1 key2 key3 = {a,b,c,d,e} 387 | 388 | .. note:: 389 | 390 | **Time complexity**: ``O(N)`` where ``N`` is the total number of 391 | elements in all given sets. 392 | 393 | Keys that do not exist are considered to be empty sets. 394 | 395 | :param keys: Two or more set keys as positional arguments 396 | :type keys: :class:`str`, :class:`bytes` 397 | :rtype: list 398 | :raises: :exc:`~tredis.exceptions.RedisError` 399 | 400 | """ 401 | return self._execute([b'SUNION'] + list(keys)) 402 | 403 | def sunionstore(self, destination, *keys): 404 | """This command is equal to :meth:`~tredis.RedisClient.sunion`, but 405 | instead of returning the resulting set, it is stored in destination. 406 | 407 | If destination already exists, it is overwritten. 408 | 409 | .. note:: 410 | 411 | **Time complexity**: ``O(N)`` where ``N`` is the total number of 412 | elements in all given sets. 413 | 414 | :param destination: The set to store the union into 415 | :type destination: :class:`str`, :class:`bytes` 416 | :param keys: One or more set keys as positional arguments 417 | :type keys: :class:`str`, :class:`bytes` 418 | :rtype: int 419 | :raises: :exc:`~tredis.exceptions.RedisError` 420 | 421 | """ 422 | return self._execute([b'SUNIONSTORE', destination] + list(keys)) 423 | -------------------------------------------------------------------------------- /tredis/sortedsets.py: -------------------------------------------------------------------------------- 1 | """Redis Sorted Set Commands Mixin""" 2 | 3 | 4 | class SortedSetsMixin(object): 5 | """Redis Sorted Set Commands Mixin""" 6 | 7 | def zadd(self, key, *members, **kwargs): 8 | """Adds all the specified members with the specified scores to the 9 | sorted set stored at key. It is possible to specify multiple score / 10 | member pairs. If a specified member is already a member of the sorted 11 | set, the score is updated and the element reinserted at the right 12 | position to ensure the correct ordering. 13 | 14 | If key does not exist, a new sorted set with the specified members as 15 | sole members is created, like if the sorted set was empty. If the key 16 | exists but does not hold a sorted set, an error is returned. 17 | 18 | The score values should be the string representation of a double 19 | precision floating point number. +inf and -inf values are valid values 20 | as well. 21 | 22 | **Members parameters** 23 | 24 | ``members`` could be either: 25 | - a single dict where keys correspond to scores and values to elements 26 | - multiple strings paired as score then element 27 | 28 | .. code:: python 29 | 30 | yield client.zadd('myzset', {'1': 'one', '2': 'two'}) 31 | yield client.zadd('myzset', '1', 'one', '2', 'two') 32 | 33 | **ZADD options (Redis 3.0.2 or greater)** 34 | 35 | ZADD supports a list of options. Options are: 36 | 37 | - ``xx``: Only update elements that already exist. Never add elements. 38 | - ``nx``: Don't update already existing elements. Always add new 39 | elements. 40 | - ``ch``: Modify the return value from the number of new elements 41 | added, to the total number of elements changed (CH is an 42 | abbreviation of changed). Changed elements are new elements added 43 | and elements already existing for which the score was updated. So 44 | elements specified in the command having the same score as they had 45 | in the past are not counted. Note: normally the return value of 46 | ``ZADD`` only counts the number of new elements added. 47 | - ``incr``: When this option is specified ``ZADD`` acts like 48 | :meth:`~tredis.RedisClient.zincrby`. Only one score-element pair 49 | can be specified in this mode. 50 | 51 | .. note:: 52 | 53 | **Time complexity**: ``O(log(N))`` for each item added, where ``N`` 54 | is the number of elements in the sorted set. 55 | 56 | :param key: The key of the sorted set 57 | :type key: :class:`str`, :class:`bytes` 58 | :param members: Elements to add 59 | :type members: :class:`dict`, :class:`str`, :class:`bytes` 60 | :keyword bool xx: Only update elements that already exist 61 | :keyword bool nx: Don't update already existing elements 62 | :keyword bool ch: Return the number of changed elements 63 | :keyword bool incr: Increment the score of an element 64 | :rtype: int, :class:`str`, :class:`bytes` 65 | :returns: Number of elements changed, or the new score if incr is set 66 | :raises: :exc:`~tredis.exceptions.RedisError` 67 | 68 | """ 69 | xx = kwargs.pop('xx', False) 70 | nx = kwargs.pop('nx', False) 71 | ch = kwargs.pop('ch', False) 72 | incr = kwargs.pop('incr', False) 73 | command = [b'ZADD', key] 74 | if xx: 75 | command += ['XX'] 76 | if nx: 77 | command += ['NX'] 78 | if ch: 79 | command += ['CH'] 80 | if incr: 81 | command += ['INCR'] 82 | 83 | if len(members) == 1: 84 | for k in members[0]: 85 | command += [k, members[0][k]] 86 | else: 87 | command += list(members) 88 | return self._execute(command) 89 | 90 | def zcard(self, key): 91 | """Returns the set cardinality (number of elements) of the sorted set 92 | stored at key. 93 | 94 | .. note:: 95 | 96 | **Time complexity**: ``O(1)`` 97 | 98 | :param key: The key of the set 99 | :type key: :class:`str`, :class:`bytes` 100 | :rtype: int 101 | :raises: :exc:`~tredis.exceptions.RedisError` 102 | 103 | """ 104 | return self._execute([b'ZCARD', key]) 105 | 106 | def zrange(self, key, start=0, stop=-1, with_scores=False): 107 | """Returns the specified range of elements in the sorted set stored at 108 | key. The elements are considered to be ordered from the lowest to the 109 | highest score. Lexicographical order is used for elements with equal 110 | score. 111 | 112 | See :meth:`tredis.Client.zrevrange` when you need the elements ordered 113 | from highest to lowest score (and descending lexicographical order for 114 | elements with equal score). 115 | 116 | Both start and stop are zero-based indexes, where ``0`` is the first 117 | element, ``1`` is the next element and so on. They can also be negative 118 | numbers indicating offsets from the end of the sorted set, with ``-1`` 119 | being the last element of the sorted set, ``-2`` the penultimate 120 | element and so on. 121 | 122 | ``start`` and ``stop`` are inclusive ranges, so for example 123 | ``ZRANGE myzset 0 1`` will return both the first and the second element 124 | of the sorted set. 125 | 126 | Out of range indexes will not produce an error. If start is larger than 127 | the largest index in the sorted set, or ``start > stop``, an empty list 128 | is returned. If stop is larger than the end of the sorted set Redis 129 | will treat it like it is the last element of the sorted set. 130 | 131 | It is possible to pass the ``WITHSCORES`` option in order to return the 132 | scores of the elements together with the elements. The returned list 133 | will contain ``value1,score1,...,valueN,scoreN`` instead of 134 | ``value1,...,valueN``. Client libraries are free to return a more 135 | appropriate data type (suggestion: an array with (value, score) 136 | arrays/tuples). 137 | 138 | .. note:: 139 | 140 | **Time complexity**: ``O(log(N)+M)`` with ``N`` being the number of 141 | elements in the sorted set and ``M`` the number of elements 142 | returned. 143 | 144 | :param key: The key of the sorted set 145 | :type key: :class:`str`, :class:`bytes` 146 | :param int start: The starting index of the sorted set 147 | :param int stop: The ending index of the sorted set 148 | :param bool with_scores: Return the scores with the elements 149 | 150 | :rtype: list 151 | :raises: :exc:`~tredis.exceptions.RedisError` 152 | """ 153 | command = [b'ZRANGE', key, start, stop] 154 | if with_scores: 155 | command += ['WITHSCORES'] 156 | return self._execute(command) 157 | 158 | def zrangebyscore(self, 159 | key, 160 | min_score, 161 | max_score, 162 | with_scores=False, 163 | offset=0, 164 | count=0): 165 | """Returns all the elements in the sorted set at key with a score 166 | between min and max (including elements with score equal to min or 167 | max). The elements are considered to be ordered from low to high 168 | scores. 169 | 170 | The elements having the same score are returned in lexicographical 171 | order (this follows from a property of the sorted set implementation in 172 | Redis and does not involve further computation). 173 | 174 | The optional ``offset`` and ``count`` arguments can be used to only get 175 | a range of the matching elements (similar to SELECT LIMIT offset, count 176 | in SQL). Keep in mind that if offset is large, the sorted set needs to 177 | be traversed for offset elements before getting to the elements to 178 | return, which can add up to ``O(N)`` time complexity. 179 | 180 | The optional ``with_scores`` argument makes the command return both the 181 | element and its score, instead of the element alone. This option is 182 | available since Redis 2.0. 183 | 184 | **Exclusive intervals and infinity** 185 | 186 | ``min_score`` and ``max_score`` can be ``-inf`` and ``+inf``, so that 187 | you are not required to know the highest or lowest score in the sorted 188 | set to get all elements from or up to a certain score. 189 | 190 | By default, the interval specified by ``min_score`` and ``max_score`` 191 | is closed (inclusive). It is possible to specify an open interval 192 | (exclusive) by prefixing the score with the character ``(``. For 193 | example: 194 | 195 | .. code:: 196 | 197 | ZRANGEBYSCORE zset (1 5 198 | 199 | Will return all elements with ``1 < score <= 5`` while: 200 | 201 | .. code:: 202 | 203 | ZRANGEBYSCORE zset (5 (10 204 | 205 | Will return all the elements with ``5 < score < 10`` (5 and 10 206 | excluded). 207 | 208 | .. note:: 209 | 210 | **Time complexity**: ``O(log(N)+M)`` with ``N`` being the number of 211 | elements in the sorted set and ``M`` the number of elements being 212 | returned. If ``M`` is constant (e.g. always asking for the first 213 | 10 elements with ``count``), you can consider it ``O(log(N))``. 214 | 215 | :param key: The key of the sorted set 216 | :type key: :class:`str`, :class:`bytes` 217 | :param min_score: Lowest score definition 218 | :type min_score: :class:`str`, :class:`bytes` 219 | :param max_score: Highest score definition 220 | :type max_score: :class:`str`, :class:`bytes` 221 | :param bool with_scores: Return elements and scores 222 | :param offset: The number of elements to skip 223 | :type min_score: :class:`str`, :class:`bytes` 224 | :param count: The number of elements to return 225 | :type min_score: :class:`str`, :class:`bytes` 226 | :rtype: list 227 | :raises: :exc:`~tredis.exceptions.RedisError` 228 | """ 229 | command = [b'ZRANGEBYSCORE', key, min_score, max_score] 230 | if with_scores: 231 | command += ['WITHSCORES'] 232 | if offset or count: 233 | command += ['LIMIT', offset, count] 234 | return self._execute(command) 235 | 236 | def zrem(self, key, *members): 237 | """Removes the specified members from the sorted set stored at key. 238 | Non existing members are ignored. 239 | 240 | An error is returned when key exists and does not hold a sorted set. 241 | 242 | .. note:: 243 | 244 | **Time complexity**: ``O(M*log(N))`` with ``N`` being the number of 245 | elements in the sorted set and ``M`` the number of elements to be 246 | removed. 247 | 248 | :param key: The key of the sorted set 249 | :type key: :class:`str`, :class:`bytes` 250 | :param members: One or more member values to remove 251 | :type members: :class:`str`, :class:`bytes` 252 | :rtype: int 253 | :raises: :exc:`~tredis.exceptions.RedisError` 254 | """ 255 | return self._execute([b'ZREM', key] + list(members)) 256 | 257 | def zremrangebyscore(self, key, min_score, max_score): 258 | """Removes all elements in the sorted set stored at key with a score 259 | between min and max. 260 | 261 | Intervals are described in :meth:`~tredis.RedisClient.zrangebyscore`. 262 | 263 | Returns the number of elements removed. 264 | 265 | .. note:: 266 | 267 | **Time complexity**: ``O(log(N)+M)`` with ``N`` being the number of 268 | elements in the sorted set and M the number of elements removed by 269 | the operation. 270 | 271 | :param key: The key of the sorted set 272 | :type key: :class:`str`, :class:`bytes` 273 | :param min_score: Lowest score definition 274 | :type min_score: :class:`str`, :class:`bytes` 275 | :param max_score: Highest score definition 276 | :type max_score: :class:`str`, :class:`bytes` 277 | :rtype: int 278 | :raises: :exc:`~tredis.exceptions.RedisError` 279 | """ 280 | return self._execute([b'ZREMRANGEBYSCORE', key, min_score, max_score]) 281 | 282 | def zrevrange(self, key, start=0, stop=-1, with_scores=False): 283 | """Returns the specified range of elements in the sorted set stored at 284 | key. The elements are considered to be ordered from the highest to the 285 | lowest score. Descending lexicographical order is used for elements 286 | with equal score. 287 | 288 | Apart from the reversed ordering, :py:meth:`~tredis.Client.zrevrange` 289 | is similar to :py:meth:`~tredis.Client.zrange` . 290 | 291 | .. note:: 292 | 293 | **Time complexity**: ``O(log(N)+M)`` with ``N`` being the number of 294 | elements in the sorted set and ``M`` the number of elements 295 | returned. 296 | 297 | :param key: The key of the sorted set 298 | :type key: :class:`str`, :class:`bytes` 299 | :param int start: The starting index of the sorted set 300 | :param int stop: The ending index of the sorted set 301 | :param bool with_scores: Return the scores with the elements 302 | 303 | :rtype: list 304 | :raises: :exc:`~tredis.exceptions.RedisError` 305 | """ 306 | command = [b'ZREVRANGE', key, start, stop] 307 | if with_scores: 308 | command += ['WITHSCORES'] 309 | return self._execute(command) 310 | 311 | def zscore(self, key, member): 312 | """Returns the score of member in the sorted set at key. 313 | If member does not exist in the sorted set, or key does not exist 314 | None is returned. 315 | 316 | .. note:: 317 | 318 | **Time complexity**: ``O(1)`` 319 | 320 | :param key: The key of the set to check for membership in 321 | :type key: :class:`str`, :class:`bytes` 322 | :param member: The value to check for set membership with 323 | :type member: :class:`str`, :class:`bytes` 324 | :rtype: str or None 325 | :raises: :exc:`~tredis.exceptions.RedisError` 326 | 327 | """ 328 | return self._execute([b'ZSCORE', key, member]) 329 | -------------------------------------------------------------------------------- /tredis/transactions.py: -------------------------------------------------------------------------------- 1 | """Redis Transaction Commands Mixin""" 2 | 3 | 4 | class TransactionsMixin(object): 5 | """Redis Transaction Commands Mixin""" 6 | pass 7 | --------------------------------------------------------------------------------