Skip to content

An async redis client for python with minimal API

License

Notifications You must be signed in to change notification settings

stereobutter/reddish

Repository files navigation

reddish - a friendly redis client with sync and async api

PyPI

Features

  • both sync and async API
  • bring your own connection - supports TCP, TCP+TLS and Unix domain sockets
  • supports multiple networking libaries and event loops
    • sync API using the standard library socket module
    • async API using asyncio's, trio's or anyio's stream primitives
  • minimal API so you don't have to relearn how to write redis commands
  • supports allmost every redis command (including modules) except for SUBSCRIBE, PSUBSCRIBE, SSUBSCRIBE, MONITOR and CLIENT TRACKING 1
  • parses replies into python types if you like (powered by pydantic)
  • works with every redis version and supports both RESP2and RESP3 protocols

Installation

pip install reddish  # install just with support for socket and asyncio
pip install reddish[trio]  # install with support for trio
pip install reddish[anyio]  # install with support for anyio

Minimal Example - sync version

import socket
from reddish import Command
from reddish.clients.socket import Redis

redis = Redis(socket.create_connection(('localhost', 6379)))

assert b'PONG' == redis.execute(Command('PING'))

Minimal Example - async version (asyncio)

import asyncio
from reddish.clients.asyncio import Redis

redis = Redis(await asyncio.open_connection('localhost', 6379))

assert b'PONG' == await redis.execute(Command('PING'))

Minimal Example - async version (trio)

import trio
from reddish.clients.trio import Redis

redis = Redis(await trio.open_tcp_stream('localhost', 6379))

assert b'PONG' == await redis.execute(Command('PING'))

Minimal Example - async version (anyio)

import trio
from reddish.backends.anyio import Redis

redis = Redis(await anyio.connect_tctp('localhost', 6379))

assert b'PONG' == await redis.execute(Command('PING'))

Usage

Command with a fixed number of arguments

# simple command without any arguments
Command('PING')

# commands with positional arguments
Command('ECHO {}', 'hello world')

# commands with keyword arguments
Command('SET {key} {value}', key='foo', value=42)

Handling invalid commands

from reddish import CommandError

try:
    await redis.execute(Command("foo"))
except CommandError as error:
    print(error.message)  # >>> ERR unknown command `foo`, with args beginning with:

Parsing replies

# return response unchanged from redis
assert b'42' == await redis.execute(Command('ECHO {}', 42))

# parse response as type
assert 42 == await redis.execute(Command('ECHO {}', 42).into(int))

# handling replies that won't parse correctly
from reddish import ParseError

try:
    await redis.execute(Command('PING').into(int))
except ParseError as error:
    print(error.reply)

# use any type that works with pydantic
from pydantic import Json
import json

data = json.dumps({'alice': 30, 'bob': 42})
response == await redis.execute(Command('ECHO {}', data).into(Json))
assert response == json.loads(data)

Commands with variadic arguments

from reddish import Args

# inlining arguments
Command('DEL {keys}', keys=Args(['foo', 'bar']))  # DEL foo bar

# inlining pairwise arguments 
data = {'name': 'bob', 'age': 42}
Command('XADD foo * {fields}', fields=Args.from_dict(data))  # XADD foo * name bob age 42

Pipelining commands

foo, bar = await redis.execute_many(Command('GET', 'foo'), Command('GET', 'bar'))

# handling errors in a pipeline
from reddish import PipelineError

try:
    foo, bar = await redis.execute_many(*commands)
except PipelineError as error:
    for outcome in error.outcomes:
        try:
            # either returns the reply if it was successful 
            # or raises the original exception if not
            value = outcome.unwrap() 
            ...
        except CommandError:
            ...
        except ParseError:
            ...

Transactions

from reddish import MultiExec

tx = MultiExec(
    Command('ECHO {}', 'foo'),
    Command('ECHO {}', 'bar')
)

foo, bar = await redis.execute(tx)

# handling errors with transactions
try:
    await redis.execute(some_tx)
except CommandError as error:
    # The exception as a whole failed and redis replied with an EXECABORT error
    cause = error.__cause__  # original CommandError that caused the EXECABORT
    print(f'Exception aborted due to {cause.code}:{cause.message}')
except TransactionError as error:
    # Some command inside the transaction failed 
    # but the transaction as a whole succeeded
    # to get at the partial results of the transaction and the errors 
    # you can iterate over the outcomes of the transaction
    for outcome in error.outcomes:
        try:
            outcome.unwrap()  # get the value or raise the original error
        except CommandError:
            ...
        except ParseError:
            ...


# pipelining together with transactions
[foo, bar], baz = await redis.execute_many(tx, Command('ECHO {}', 'baz'))

# handling errors with transactions inside a pipeline
try:
    res: list[int] = await redis.execute_many(*cmds)
except PipelineError as error:
    # handle the outcomes of the pipeline
    for outcome in error.outcomes:
        try:
            outcome.unwrap()
        except CommandError:
            ...
        except ParseError:
            ...
        except TransactionError as tx_error:
            # handle errors inside the transaction
            for tx_outcome in tx_error.outcomes:
                ...

Footnotes

  1. Commands like SUBSCRIBE or MONITOR take over the redis connection for listening to new events barring regular commands from being issued over the connection.

About

An async redis client for python with minimal API

Topics

Resources

License

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •  

Languages