Greyhorse Redis library
Greyhorse framework library for Redis support.
The primary API is the pieces, not a ready-made module:
| Piece | Sync | Async |
|---|---|---|
| material (the engine's own factory) | RedisSyncFragment |
RedisAsyncFragment |
| lifecycle (start/stop/health) | RedisSyncBorder |
RedisAsyncBorder |
| access (hand out clients/pipelines) | RedisSyncConnections |
RedisAsyncConnections |
An application lists the ones it needs on its own Module, alongside
pieces from any other storage library. RedisSyncModule/RedisAsyncModule
bundle the trio for the common single-storage case -- sugar, not the primary
surface. See examples/.
Usage
Every snippet below is a runnable program, checked against a live Redis. The
longer, commented versions live in examples/ and are executed by the test
suite, so they cannot rot silently.
One engine, one client
RedisSyncModule is the ready-made bundle for the single-storage case. The
config reaches the engine's constructor through the same args={Type: value}
door every greyhorse.strand resource uses -- there is no redis-specific
wiring.
from greyhorse.run import wrap_sync
from greyhorse.strand import running
from greyhorse_redis import EngineConf, RedisSyncClientCtx, RedisSyncModule
def main() -> None:
conf = EngineConf(dsn='redis://localhost:6379/0')
with running(RedisSyncModule, args={EngineConf: conf}) as module:
client_ctx = module.get(RedisSyncClientCtx).unwrap()
with client_ctx as client:
client.set('greeting', 'hello')
print(client.get('greeting'))
wrap_sync(main)
The engine starts when the module starts and stops when it stops; client is
an ordinary redis.Redis borrowed for the length of the with block.
Async
Same declaration, async twin of each piece. running() stays a plain sync
context manager even here -- it starts a module, it is not an I/O operation.
from greyhorse.run import run
from greyhorse.strand import running
from greyhorse_redis import EngineConf, RedisAsyncClientCtx, RedisAsyncModule
async def main() -> None:
conf = EngineConf(dsn='redis://localhost:6379/0')
with running(RedisAsyncModule, args={EngineConf: conf}) as module:
client_ctx = module.get(RedisAsyncClientCtx).unwrap()
async with client_ctx as client:
await client.set('greeting', 'hello')
print(await client.get('greeting'))
run(main)
Pipelines
A client is Shared: every command takes effect the moment the server sees
it, so there is nothing to commit. A pipeline is Mut -- commands buffer
while the window is open and apply() sends MULTI/EXEC.
pipe_ctx = module.get(RedisSyncPipelineCtx).unwrap()
with pipe_ctx as pipe:
pipe.set('a', '1')
pipe.set('b', '2')
pipe_ctx.apply() # both writes land here, or neither
A forgotten apply() and an exception inside the block both discard the
buffer without sending anything. That is the whole reason a pipeline is Mut
and a client is not.
A consumer that knows nothing about greyhorse
The point of the split: the class that uses Redis takes a context by TYPE and imports nothing from this package. Only the component says how it is wired.
from typing import ClassVar
from greyhorse.strand import (
Application, Component, Handle, HttpBinding, HttpGateway, Shared, Use,
)
from greyhorse_redis import (
EngineConf, RedisSyncClientCtx, RedisSyncConnections, RedisSyncModule,
SyncRedisEngine,
)
class PingApi:
def __init__(self, conn: RedisSyncClientCtx) -> None:
self._conn = conn
def ping(self) -> bool:
with self._conn as client:
return bool(client.ping())
class PingComponent(Component):
imports: ClassVar = Shared[SyncRedisEngine]
providers: ClassVar = RedisSyncConnections
exports: ClassVar = PingApi
handlers: ClassVar = Handle(PingApi.ping, HttpBinding.Route(verb='GET', path=''))
class App(RedisSyncModule):
name = 'ping-app'
components: ClassVar = {'ping': Use(PingComponent)}
app = Application(App, args={EngineConf: EngineConf(dsn='redis://localhost:6379/0')},
gateways=(HttpGateway(),))
with app.started() as running_app:
print(running_app.gateway(HttpGateway).dispatch('GET', '/ping', {}))
imports/providers are the demand side -- the component asks for a window
onto the engine and names the provider that turns it into a context.
exports/handlers are what it hands back out. Note that an export reaches
consumers through the wiring, NOT through module.get(): that door serves the
module's own products (the contexts above), not a component's exports.
Subclassing RedisSyncModule fits exactly this shape: one storage, one
consumer, same floor. For two independent Redis instances, list the pieces
(RedisSyncFragment, RedisSyncBorder, RedisSyncConnections) on your own
Module instead -- examples/04_multi_storage.py does that in full.
Configuration
EngineConf is what the engine is built from:
EngineConf(
dsn='redis://user:password@localhost:6379/0',
timeout_seconds=5,
connect_timeout_seconds=5,
pool_max_connections=4,
client_name='greyhorse-redis',
decode_responses=False,
)
rediss:// (TLS) and unix:///var/run/redis.sock are accepted too.
RedisSettings is the environment side. It reads REDIS_* (and .env), and
assembles a DSN from the parts when REDIS_DSN is not given:
from greyhorse_redis import EngineConf, RedisSettings
settings = RedisSettings() # REDIS_HOST, REDIS_PORT, ...
conf = EngineConf(dsn=settings.dsn)
REDIS_PASSWORD_FILE points at a Docker/Kubernetes secret file and wins over
REDIS_PASSWORD. A missing, unreadable or blank file is an error at config
time, not a silent fallback to no password.
Credentials. repr(), str() and model_dump_json() come out redacted --
host and user stay visible, the secret does not -- so a config that reaches a
log, an f-string or a traceback does not leak. model_dump() and
dict(settings) deliberately keep the credential: that is what builds the
connection pool. Dumps are for machines; do not log one.
How to build
-
Install the project
uv venvuv sync --extra hiredissource .venv/bin/activate -
Format code commands
ruff check --unsafe-fixes --fixruff format -
Run the tests
Everything except the live-server tests runs with no Redis at all. For the rest, bring one up and point the suite at it:
docker compose -f tests/docker-compose.yml up -d --waitREDIS_TEST_URI=redis://localhost:6380/15 uv run pytest tests -qdocker compose -f tests/docker-compose.yml down -vHost port 6380 is a default, not a fixture: if it is taken on your machine, set
REDIS_HOST_PORTand point the suite at the same number.REDIS_HOST_PORT=6390 docker compose -f tests/docker-compose.yml up -d --waitREDIS_TEST_URI=redis://localhost:6390/15 uv run pytest tests -q
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file greyhorse_redis-0.5.5.tar.gz.
File metadata
- Download URL: greyhorse_redis-0.5.5.tar.gz
- Upload date:
- Size: 175.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fb76f7bbea8173532c88cf55160849ba982f8b1c7c1393376d82c9086092d41
|
|
| MD5 |
7d1f829959ebeea51a1e0c600a1f7895
|
|
| BLAKE2b-256 |
b56eddcc0cf10e87b6467901d299c91325ddea9fdc4886dbbc22b83d8550a6e5
|
File details
Details for the file greyhorse_redis-0.5.5-py3-none-any.whl.
File metadata
- Download URL: greyhorse_redis-0.5.5-py3-none-any.whl
- Upload date:
- Size: 59.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
441490d3d3a0f716543528b4dede43e05aea6b22415bdff9764b2a90c7c8855d
|
|
| MD5 |
e9ca03273f1625bd7f03875bfe918d71
|
|
| BLAKE2b-256 |
aea380802daa9571130721950d8f57ab8e1331360f458dbfb67f402ca785bdb6
|