Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use znjson package #9

Merged
merged 6 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ assert c.get("name") == "Fabian"
```

> [!NOTE]
> ZnSocket does not decode strings automatically. Using it is equivalent to using `Redis.from_url(storage, decode_responses=True)` in the Redis client.
> ZnSocket does not encode/decode strings. Using it is equivalent to using `Redis.from_url(storage, decode_responses=True)` in the Redis client.


## Lists
Expand Down
64 changes: 60 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "znsocket"
version = "0.1.2"
version = "0.1.3"
description = "Python implementation of a Redis-compatible API using websockets."
authors = ["Fabian Zills <[email protected]>"]
license = "Apache-2.0"
Expand All @@ -11,12 +11,14 @@ python = "^3.10"
python-socketio = {extras = ["client"], version = "^5"}
eventlet = "^0"
typer = "^0"
znjson = "^0.2.3"

[tool.poetry.group.dev.dependencies]
ruff = "^0.4"
pytest = "^8.2"
coverage = "^7.5.1"
redis = "^5"
numpy = "^1"

[build-system]
requires = ["poetry-core"]
Expand Down
16 changes: 16 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest

from znsocket import Client, exceptions


def test_client_from_url(eventlet_memory_server):
r = Client.from_url(eventlet_memory_server)
r.set("name", "Alice")
assert r.get("name") == "Alice"


def test_client_connection_error():
with pytest.raises(
exceptions.ConnectionError, match="Could not connect to http://127.0.0.1:5000"
):
Client.from_url("znsocket://127.0.0.1:5000")
32 changes: 32 additions & 0 deletions tests/test_list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import numpy as np
import numpy.testing as npt
import pytest

import znsocket
Expand All @@ -21,6 +23,13 @@ def test_list_extend(client, request):
assert lst == ["1", "2", "3", "4"]
assert lst[:] == ["1", "2", "3", "4"]

lst.clear()
lst.extend([1, 2, 3, 4])
assert lst == [1, 2, 3, 4]

lst.extend([5, 6.28, "7"])
assert lst == [1, 2, 3, 4, 5, 6.28, "7"]


@pytest.mark.parametrize("client", ["znsclient", "redisclient", "empty"])
def test_list_setitem(client, request):
Expand Down Expand Up @@ -125,6 +134,11 @@ def test_list_iter(client, request):
lst = []
lst.extend(["1", "2", "3", "4"])

assert lst[0] == "1"
assert lst[1] == "2"
assert lst[2] == "3"
assert lst[3] == "4"

for a, b in zip(lst, ["1", "2", "3", "4"]):
assert a == b

Expand Down Expand Up @@ -182,3 +196,21 @@ def test_list_getitem(client, request):

with pytest.raises(IndexError):
lst[10]


@pytest.mark.parametrize("client", ["znsclient", "redisclient", "empty"])
def test_list_numpy(client, request):
"""Test ZnSocket with numpy arrays through znjson."""
c = request.getfixturevalue(client)
if c is not None:
lst = znsocket.List(r=c, key="list:test")
else:
lst = []

lst.extend([np.array([1, 2, 3]), np.array([4, 5, 6])])
npt.assert_array_equal(lst[0], np.array([1, 2, 3]))
npt.assert_array_equal(lst[1], np.array([4, 5, 6]))

lst[1] = np.array([7, 8, 9])
npt.assert_array_equal(lst[0], np.array([1, 2, 3]))
npt.assert_array_equal(lst[1], np.array([7, 8, 9]))
22 changes: 15 additions & 7 deletions znsocket/client.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import dataclasses

import socketio
import socketio.exceptions

from znsocket import exceptions


@dataclasses.dataclass
@dataclasses.dataclass(frozen=True)
class Client:
address: str
sio: socketio.SimpleClient = dataclasses.field(default=None, repr=False, init=False)
decode_responses: bool = True
sio: socketio.SimpleClient = dataclasses.field(
default_factory=socketio.SimpleClient, repr=False, init=False
)

@classmethod
def from_url(cls, url):
def from_url(cls, url, **kwargs) -> "Client":
"""Connect to a znsocket server using a URL.

Parameters
Expand All @@ -20,11 +23,16 @@ def from_url(cls, url):
The URL of the znsocket server. Should be in the format
"znsocket://127.0.0.1:5000".
"""
return cls(address=url.replace("znsocket://", "http://"))
return cls(address=url.replace("znsocket://", "http://"), **kwargs)

def __post_init__(self):
self.sio = socketio.SimpleClient()
self.sio.connect(self.address)
try:
self.sio.connect(self.address)
except socketio.exceptions.ConnectionError as err:
raise exceptions.ConnectionError(self.address) from err

if not self.decode_responses:
raise NotImplementedError("decode_responses=False is not supported yet")

def delete(self, name):
return self.sio.call("delete", {"name": name})
Expand Down
11 changes: 11 additions & 0 deletions znsocket/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ def __init__(self, response: str):

def __str__(self):
return self.response


class ConnectionError(Exception):
def __init__(self, address: str):
self.address = address

def __str__(self):
response = f"Could not connect to {self.address}. "
response += "Is the 'znsocket' server running? "
response += "You can start it using the CLI 'znsocket'."
return response
26 changes: 16 additions & 10 deletions znsocket/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import json
import typing as t
from collections.abc import MutableSequence

import znjson

from .client import Client


Expand Down Expand Up @@ -38,7 +41,11 @@ def __getitem__(self, index: int | list | slice):

items = []
for i in index:
item = self.redis.lindex(self.key, i)
value = self.redis.lindex(self.key, i)
try:
item = znjson.loads(value)
except TypeError:
item = value
if item is None:
raise IndexError("list index out of range")
items.append(item)
Expand All @@ -48,7 +55,6 @@ def __setitem__(self, index: int | list | slice, value: str | list[str]):
single_item = isinstance(index, int)
if single_item:
index = [index]
assert isinstance(value, str), "single index requires single value"
value = [value]

if isinstance(index, slice):
Expand All @@ -64,7 +70,7 @@ def __setitem__(self, index: int | list | slice, value: str | list[str]):
for i, v in zip(index, value):
if i >= self.__len__() or i < -self.__len__():
raise IndexError("list index out of range")
self.redis.lset(self.key, i, v)
self.redis.lset(self.key, i, znjson.dumps(v))

def __delitem__(self, index: int | list | slice):
single_item = isinstance(index, int)
Expand All @@ -79,15 +85,12 @@ def __delitem__(self, index: int | list | slice):

def insert(self, index, value):
if index >= self.__len__():
self.redis.rpush(self.key, value)
self.redis.rpush(self.key, znjson.dumps(value))
elif index == 0:
self.redis.lpush(self.key, value)
self.redis.lpush(self.key, znjson.dumps(value))
else:
pivot = self.redis.lindex(self.key, index)
self.redis.linsert(self.key, "BEFORE", pivot, value)

def __iter__(self):
return (item for item in self.redis.lrange(self.key, 0, -1))
self.redis.linsert(self.key, "BEFORE", pivot, znjson.dumps(value))

def __eq__(self, value: object) -> bool:
if isinstance(value, List):
Expand All @@ -97,4 +100,7 @@ def __eq__(self, value: object) -> bool:
return False

def __repr__(self):
return f"List({self.redis.lrange(self.key, 0, -1)})"
data = self.redis.lrange(self.key, 0, -1)
data = [znjson.loads(i) for i in data]

return f"List({data})"
Loading