├── .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 |
--------------------------------------------------------------------------------