Redis+Trio support
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
|
||||
|
||||
Code here is borrowed from [alekseyev/trio-redis](https://github.com/alekseyev/trio-redis), which was originally developed over at [Bogdanp/trio-redis](https://github.com/Bogdanp/trio-redis).
|
||||
|
||||
Since it has no active maintainers and no PyPI package - I am including it as-is.
|
||||
|
||||
## Usage
|
||||
|
||||
```python3
|
||||
from quart_session.redis_trio import RedisTrio
|
||||
cache = RedisTrio(
|
||||
addr=b"10.0.0.3", port=6379, password=b"foo")
|
||||
await cache.connect()
|
||||
|
||||
await cache.setex(key="foo", value=42, seconds=300)
|
||||
await cache.get("foo")
|
||||
```
|
||||
|
||||
Or,
|
||||
|
||||
```python3
|
||||
async with RedisTrio() as cache:
|
||||
await cache.set("foo", 42)
|
||||
await cache.get("foo")
|
||||
```
|
||||
|
||||
## Future work
|
||||
|
||||
If someone makes a Redis+Trio client that supports connection pooling, we can switch to it.
|
||||
|
||||
|
||||
```
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
quart_session.redis_trio
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple Redis Trio client.
|
||||
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from quart_session.redis_trio.client import RedisTrio
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
quart_session.redis_trio
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple Redis Trio client.
|
||||
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
from .connection import RedisConnection
|
||||
|
||||
|
||||
class RedisTrio:
|
||||
"""A simple Redis+Trio client.
|
||||
|
||||
Parameters:
|
||||
addr(str): The IP address the Redis server is listening on.
|
||||
port(int): The port the Redis server is listening on.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> async with RedisTrio() as redis:
|
||||
... await redis.set("foo", 42)
|
||||
... await redis.get("foo")
|
||||
b'42'
|
||||
"""
|
||||
|
||||
def __init__(self, addr: Union[bytes, str] = b"127.0.0.1", port: int = 6379, password: bytes = b""):
|
||||
self.conn = RedisConnection(addr, port)
|
||||
self.password = password
|
||||
|
||||
async def connect(self):
|
||||
"""Open a connection to the Redis server.
|
||||
|
||||
Returns:
|
||||
RedisTrio: This instance.
|
||||
"""
|
||||
await self.conn.connect()
|
||||
if self.password:
|
||||
await self.auth(self.password)
|
||||
return self
|
||||
|
||||
async def close(self):
|
||||
"""Close the connection to the Redis server.
|
||||
"""
|
||||
await self.quit()
|
||||
self.conn.close()
|
||||
|
||||
async def auth(self, password):
|
||||
return await self.conn.process_command_ok(b"AUTH", password)
|
||||
|
||||
async def delete(self, *keys):
|
||||
return await self.conn.process_command(b"DEL", *keys)
|
||||
|
||||
async def echo(self, message):
|
||||
return await self.conn.process_command(b"ECHO", message)
|
||||
|
||||
async def flushall(self):
|
||||
return await self.conn.process_command_ok(b"FLUSHALL")
|
||||
|
||||
async def get(self, key) -> bytes:
|
||||
return await self.conn.process_command(b"GET", key)
|
||||
|
||||
async def quit(self):
|
||||
return await self.conn.process_command(b"QUIT")
|
||||
|
||||
async def set(self, key, value):
|
||||
return await self.conn.process_command_ok(b"SET", key, value)
|
||||
|
||||
async def setex(self, key: str, value: str, seconds: int):
|
||||
"""Set the value and expiration of a key.
|
||||
:raises TypeError: if seconds is not int
|
||||
"""
|
||||
if not isinstance(seconds, int):
|
||||
raise TypeError("milliseconds argument must be int")
|
||||
|
||||
return await self.conn.process_command_ok(b"SETEX", key, seconds, value)
|
||||
|
||||
async def __aenter__(self):
|
||||
return await self.connect()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
quart_session.redis_trio.connection
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple Redis Trio client.
|
||||
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
import trio
|
||||
|
||||
from .serialization import atom, serialize
|
||||
from .errors import ProtocolError, ResponseError, ResponseTypeError
|
||||
|
||||
|
||||
SP = ord("+")
|
||||
EP = ord("-")
|
||||
IP = ord(":")
|
||||
BP = ord("$")
|
||||
AP = ord("*")
|
||||
|
||||
#: The set of known Redis response prefixes.
|
||||
known_prefixes = {SP, EP, IP, BP, AP}
|
||||
|
||||
|
||||
class ReadMore(Exception):
|
||||
"""Raised by parse to signal that it needs more data.
|
||||
"""
|
||||
|
||||
|
||||
class RedisConnection:
|
||||
"""This class facilitates all communication with Redis via a trio socket.
|
||||
Warning:
|
||||
The interface of this class may change at any time, without
|
||||
notice, due to the experimental nature of Trio.
|
||||
"""
|
||||
|
||||
def __init__(self, addr: Union[bytes, str], port: int, bufsize: int = 16384):
|
||||
self.addr = (addr, port)
|
||||
self.sock = trio.socket.socket()
|
||||
self.bufsize = bufsize
|
||||
|
||||
async def connect(self):
|
||||
await self.sock.connect(self.addr)
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
async def send_command(self, command, *args):
|
||||
command_and_args = (serialize(arg) for arg in (atom(command),) + args)
|
||||
data = b" ".join(command_and_args) + b"\r\n"
|
||||
await self.sock.send(data)
|
||||
|
||||
async def process_command(self, *command_and_args):
|
||||
await self.send_command(*command_and_args)
|
||||
return await self.process_response()
|
||||
|
||||
async def process_command_ok(self, *command_and_args):
|
||||
await self.send_command(*command_and_args)
|
||||
return await self.process_response() == b"OK"
|
||||
|
||||
async def process_response(self):
|
||||
data = await self.sock.recv(self.bufsize)
|
||||
while True:
|
||||
try:
|
||||
item, _ = await self.parse(data)
|
||||
return item
|
||||
except ReadMore:
|
||||
data += await self.sock.recv(self.bufsize)
|
||||
|
||||
async def parse(self, data):
|
||||
try:
|
||||
index = data.index(b"\r\n")
|
||||
except ValueError:
|
||||
raise ReadMore()
|
||||
|
||||
if data[0] not in known_prefixes:
|
||||
raise ProtocolError(f"Unexpected data in response: {data!r}.")
|
||||
|
||||
elif data[0] == SP:
|
||||
return data[1:index], data[index + 2:]
|
||||
|
||||
elif data[0] == EP:
|
||||
error = data[1:index].decode("ascii")
|
||||
if error.startswith("WRONGTYPE"):
|
||||
raise ResponseTypeError(error[len("WRONGTYPE "):])
|
||||
|
||||
elif error.startswith("ERR"):
|
||||
raise ResponseError(error[len("ERR "):])
|
||||
|
||||
else:
|
||||
raise ResponseError(error)
|
||||
|
||||
elif data[0] == IP:
|
||||
return int(data[1:index]), data[index + 2:]
|
||||
|
||||
elif data[0] == BP:
|
||||
length, data = int(data[1:index]), data[index + 2:]
|
||||
if length == -1:
|
||||
return None, data
|
||||
|
||||
elif len(data) < length + 2:
|
||||
raise ReadMore()
|
||||
|
||||
return data[:length], data[length + 2:]
|
||||
|
||||
elif data[0] == AP:
|
||||
length, data = int(data[1:index]), data[index + 2:]
|
||||
if length == -1:
|
||||
return None, data
|
||||
|
||||
return await self.parse_array(length, data)
|
||||
|
||||
else: # pragma: no cover
|
||||
assert False, "unreachable"
|
||||
|
||||
async def parse_array(self, length, data):
|
||||
items = []
|
||||
while len(items) < length:
|
||||
if not data:
|
||||
data += await self.sock.recv(self.bufsize)
|
||||
continue
|
||||
|
||||
try:
|
||||
item, data = await self.parse(data)
|
||||
items.append(item)
|
||||
except ReadMore:
|
||||
data += await self.sock.recv(self.bufsize)
|
||||
|
||||
return items, data
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
quart_session.redis_trio.errors
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple Redis Trio client.
|
||||
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
class RedisError(Exception):
|
||||
"""Base class for all Redis-related errors.
|
||||
"""
|
||||
|
||||
|
||||
class ProtocolError(RedisError):
|
||||
"""Raised when Redis responds with something that doesn't conform
|
||||
to the protocol.
|
||||
"""
|
||||
|
||||
|
||||
class ResponseError(RedisError):
|
||||
"""Raised when Redis returns an error response.
|
||||
"""
|
||||
|
||||
|
||||
class ResponseTypeError(ResponseError):
|
||||
"""Raised when Redis returns an error response with a `WRONGTYPE` prefix.
|
||||
"""
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
quart_session.redis_trio.serialization
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple Redis Trio client.
|
||||
|
||||
:copyright: (c) 2017 by Bogdan Paul Popa.
|
||||
:copyright: (c) 2019 by Oleksii Aleksieiev.
|
||||
:copyright: (c) 2020 by dsc.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
#: Wrapper class for values that don't have to be quoted.
|
||||
atom = namedtuple("atom", ("value",))
|
||||
|
||||
#: The set of characters that must be escaped before being sent as
|
||||
#: Redis strings.
|
||||
escapes = {
|
||||
ord(b"\0"): rb"\x00",
|
||||
ord(b"\n"): rb"\n",
|
||||
ord(b"\r"): rb"\r",
|
||||
ord(b"\\"): rb"\\",
|
||||
ord(b'"'): rb'\"',
|
||||
}
|
||||
|
||||
|
||||
def serialize(x):
|
||||
"""Serialize `x` so that it can safely be sent to Redis.
|
||||
|
||||
Parameters:
|
||||
x(object): The value to serialize.
|
||||
|
||||
Returns:
|
||||
bytes: The serialized value.
|
||||
"""
|
||||
if isinstance(x, atom):
|
||||
return x.value
|
||||
elif isinstance(x, bytes):
|
||||
return quote(x)
|
||||
elif isinstance(x, str):
|
||||
return quote(x.encode("utf-8"))
|
||||
elif isinstance(x, (float, int)):
|
||||
return str(x).encode("ascii")
|
||||
else:
|
||||
return serialize(str(x))
|
||||
|
||||
|
||||
def quote(bs):
|
||||
return b'"' + bytes(escape(bs)) + b'"'
|
||||
|
||||
|
||||
def escape(bs):
|
||||
for c in bs:
|
||||
if c in escapes:
|
||||
yield from escapes[c]
|
||||
else:
|
||||
yield c
|
||||
Reference in New Issue
Block a user