From 0341ece82e4c95252f5d39182bec7155a1196c53 Mon Sep 17 00:00:00 2001 From: Eugene Molotov Date: Sat, 10 Aug 2024 20:00:50 +0500 Subject: [PATCH 1/2] .gitignore: ignore pyc files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a346a3..4f9e7f2 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ *.app bin/minqlx.zip +*.pyc From 5dd394a5bfa319b724be22e535eb5dd065b04e36 Mon Sep 17 00:00:00 2001 From: Eugene Molotov Date: Sat, 10 Aug 2024 17:52:11 +0500 Subject: [PATCH 2/2] Finally support redis-py 3.* package or later MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds compatibility layer, so old plugins do not need to adapt changes to support >3.* python3 package of redis. Some portions of that layer code is copy-pasted from shinqlx. Co-authored-By: Markus Gärtner --- .github/workflows/test.yml | 28 +++++++ python/minqlx/database.py | 48 ++++++++++- python/tests/database.py | 1 + python/tests/test_database.py | 154 ++++++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 120000 python/tests/database.py create mode 100644 python/tests/test_database.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4760119 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + PIP_BREAK_SYSTEM_PACKAGES: "1" + +jobs: + test_redis: + + strategy: + matrix: + redis-package-version: ["2.*", "3.*", "4.*", "5.*"] + os: ["ubuntu-24.04", "ubuntu-22.04", "ubuntu-20.04"] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Start Redis + uses: superchargejs/redis-github-action@1.1.0 + - name: Install redis package + run: python3 -m pip install redis==${{ matrix.redis-package-version }} + - name: Run test + run: python3 -m unittest discover python/tests/ diff --git a/python/minqlx/database.py b/python/minqlx/database.py index 9f42c00..abffaa7 100644 --- a/python/minqlx/database.py +++ b/python/minqlx/database.py @@ -16,7 +16,11 @@ # You should have received a copy of the GNU General Public License # along with minqlx. If not, see . -import minqlx +try: + import minqlx +except ImportError: + pass + import redis # ==================================================================== @@ -313,3 +317,45 @@ def close(self): if Redis._pool: Redis._pool.disconnect() Redis._pool = None + + if redis.VERSION > (3,): + def zincrby(self, name, value, amount=1): + return self.r.zincrby(name, amount, value) + + def zadd(self, name, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], dict): + # redis >= 3.* arguments given + return self.r.zadd(name, *args, **kwargs) + + if len(args) > 0 and len(args) % 2 != 0: + raise redis.RedisError("ZADD requires an equal number of values and scores") + + mapping = {} + for i in range(0, len(args), 2): + mapping[args[i + 1]] = args[i] + + return self.r.zadd(name, mapping, **kwargs) + + def msetnx(self, *args, **kwargs): + mapping = {} + if args: + if len(args) != 1 or not isinstance(args[0], dict): + raise redis.RedisError("MSETNX requires **kwargs or a single dict arg") + mapping.update(args[0]) + + if kwargs: + mapping.update(kwargs) + + return self.r.msetnx(mapping) + + def mset(self, *args, **kwargs): + mapping = {} + if args: + if len(args) != 1 or not isinstance(args[0], dict): + raise redis.RedisError("MSET requires **kwargs or a single dict arg") + mapping.update(args[0]) + + if kwargs: + mapping.update(kwargs) + + return self.r.mset(mapping) diff --git a/python/tests/database.py b/python/tests/database.py new file mode 120000 index 0000000..69549ee --- /dev/null +++ b/python/tests/database.py @@ -0,0 +1 @@ +../minqlx/database.py \ No newline at end of file diff --git a/python/tests/test_database.py b/python/tests/test_database.py new file mode 100644 index 0000000..9109c4c --- /dev/null +++ b/python/tests/test_database.py @@ -0,0 +1,154 @@ +import random +import string +from time import sleep +from unittest import TestCase + +import redis + +import database + + +def random_string(): + return "".join([random.choice(string.ascii_letters) for x in range(10)]) + + +class RedisRegressionTestCase(TestCase): + def setUp(self): + self.c = database.Redis(None) + self.c.connect("127.0.0.1:6379") + + def test_zadd_01(self): + c = self.c + key = "zadd_01_key_" + random_string() + + c.zadd(key, 5, "player1", 6, "player2") + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["player1"], 5) + self.assertEqual(res["player2"], 6) + + c.zadd(key, 2, "player1") + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["player1"], 2) + self.assertEqual(res["player2"], 6) + + c.zadd(key, 4, "player2") + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["player1"], 2) + self.assertEqual(res["player2"], 4) + + def test_zadd_02(self): + c = self.c + key = "zadd_02_key_" + random_string() + + # redis-py 3.* introduced xx, nx, ch and incr kwarg params + # this codes deliberately does not work in redis-py 2.* + if redis.VERSION < (3,): + self.skipTest("Old version of redis package: %s" % (redis.VERSION,)) + c.zadd(key, 5, "player1", 6, "player2") + c.zadd(key, {"player1": 3}, xx=True) + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["player1"], 3) + + c.zadd(key, 3, "player_does_not_exist", xx=True) + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertNotIn("player_does_not_exist", res) + + def test_zincrby_01(self): + key = "zincrby_01_key_" + random_string() + c = self.c + + c.zincrby(key, "FIELD1") + c.zincrby(key, "FIELD2", 2) + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["FIELD1"], 1) + self.assertEqual(res["FIELD2"], 2) + + c.zincrby(key, "FIELD1") + c.zincrby(key, "FIELD2", 3) + res = dict(c.zrange(key, 0, -1, withscores=True)) + self.assertEqual(res["FIELD1"], 2) + self.assertEqual(res["FIELD2"], 5) + + def test_lrem_01(self): + key = "lrem_01_key_" + random_string() + c = self.c + + c.rpush( + key, + "Cat", + "Dog", + "Horse", + "Cat", + "Dog", + "Cat", + "Monkey", + "Dog", + "Cat", + "Dog", + "Buffalo", + ) + c.lrem(key, 2, "Cat") + result = c.lrange(key, 0, -1) + self.assertEqual( + result, + ["Dog", "Horse", "Dog", "Cat", "Monkey", "Dog", "Cat", "Dog", "Buffalo"], + ) + + def test_setex_01(self): + key = "setex_01_key_" + random_string() + c = self.c + test_value = "test value" + timeout = 1 + + c.setex(key, timeout, test_value) + self.assertEqual(c.get(key), test_value, "Too small timeout value?") + sleep(timeout + 0.5) + self.assertEqual(c.get(key), None) + + def test_mset_01(self): + c = self.c + c.mset(field1="example1", field2="example2") + self.assertEqual(["example1", "example2"], c.mget("field1", "field2")) + + def test_mset_02(self): + c = self.c + c.mset({"field3": "example1", "field4": "example2"}) + self.assertEqual(["example1", "example2"], c.mget("field3", "field4")) + + def test_msetnx_01_kwargs(self): + c = self.c + prefix = "msetnx_01_key_" + random_string() + key1 = prefix + "_field1" + key2 = prefix + "_field2" + d = { + key1: "example1", + key2: "example2", + } + c.msetnx(**d) + self.assertEqual(["example1", "example2"], c.mget(key1, key2)) + + d = { + key1: "value should not be used", + key2: "same here", + } + c.msetnx(**d) + self.assertEqual(["example1", "example2"], c.mget(key1, key2)) + + def test_msetnx_02_dict(self): + c = self.c + prefix = "msetnx_02_key_" + random_string() + key1 = prefix + "_field1" + key2 = prefix + "_field2" + d = { + key1: "example1", + key2: "example2", + } + c.msetnx(d) + self.assertEqual(["example1", "example2"], c.mget(key1, key2)) + + d = { + key1: "value should not be used", + key2: "same here", + } + c.msetnx(d) + self.assertEqual(["example1", "example2"], c.mget(key1, key2))