Skip to content
This repository was archived by the owner on Jun 21, 2025. It is now read-only.

Commit 6db9a0f

Browse files
feat: redis-separator
#192 #277 This adds the ability to customize the redis separator and changes the default separator from _%&_ to :. Additionally, it adds a way for a user to add a prefix to the key space we use, to further differentiate their keys. Finally, it fixes a bug with testing where redislite would not shutdown properly and updates the fixtures to work with the newest version of pytest_asyncio. BREAKING CHANGE: This will result in "data loss" for existing models stored in redis due to the change in default separator. To maintain backwards compatbility with 0.7.0 and below, you will need to modify your existing models to set _redis_separator = "_%&_" as a field on them.
1 parent 0ca42e4 commit 6db9a0f

File tree

8 files changed

+73
-42
lines changed

8 files changed

+73
-42
lines changed

.github/workflows/constraints.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ nox-poetry==1.0.1
44
poetry==1.2.0
55
virtualenv==20.16.5
66
poetry-dynamic-versioning==0.18.0
7+
toml

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ loop = asyncio.get_event_loop()
113113
loop.run_until_complete(work_with_orm())
114114
```
115115

116+
#### Custom Fields in Model
117+
118+
| Field Name | Required | Default | Description |
119+
| ------------------- | -------- | ------------ | -------------------------------------------------------------------- |
120+
| \_primary_key_field | Yes | None | The field of your model that is the primary key |
121+
| \_redis_prefix | No | None | If set, will be added to the beginning of the keys we store in redis |
122+
| \_redis_separator | No | : | Defaults to :, used to separate prefix, table_name, and primary_key |
123+
| \_table_name | NO | cls.**name** | Defaults to the model's name, can set a custom name in redis |
124+
116125
## Contributing
117126

118127
Contributions are very welcome.

noxfile.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from textwrap import dedent
88

99
import nox
10+
import toml
1011

1112
try:
1213
from nox_poetry import Session
@@ -35,24 +36,8 @@
3536
"docs-build",
3637
)
3738
mypy_type_packages = ()
38-
test_requirements = (
39-
"coverage[toml]",
40-
"pytest",
41-
"pygments",
42-
"fastapi>=0.6.3",
43-
"fastapi-crudrouter>=0.8.4",
44-
"httpx",
45-
"pytest-asyncio",
46-
"pytest-cov",
47-
"pytest-env",
48-
"pytest-lazy-fixture",
49-
"pytest-mock",
50-
"pytest-mockservers",
51-
"pytest-xdist",
52-
"redislite",
53-
"pytest-asyncio",
54-
"pytest-lazy-fixture",
55-
)
39+
pyproject = toml.load("pyproject.toml")
40+
test_requirements = pyproject["tool"]["poetry"]["dev-dependencies"].keys()
5641

5742

5843
def activate_virtualenv_in_precommit_hooks(session: Session) -> None:

pydantic_aioredis/model.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,54 @@
1313
class Model(_AbstractModel):
1414
"""
1515
The section in the store that saves rows of the same kind
16+
17+
Model has some custom fields you can set in your models that alter the behavior of how this is stored in redis
18+
19+
_primary_key_field -- The field of your model that is the primary key
20+
_redis_prefix -- If set, will be added to the beginning of the keys we store in redis
21+
_redis_separator -- Defaults to :, used to separate prefix, table_name, and primary_key
22+
_table_name -- Defaults to the model's name, can set a custom name in redis
23+
24+
25+
If your model was named ThisModel, the primary key was "key", and prefix and separator were left at default (not set), the
26+
keys stored in redis would be
27+
thismodel:key
1628
"""
1729

30+
@classmethod
31+
@property
32+
def _prefix(cls):
33+
return getattr(cls, "_redis_prefix", "").lower()
34+
35+
@classmethod
36+
@property
37+
def _separator(cls):
38+
return getattr(cls, "_redis_separator", ":").lower()
39+
40+
@classmethod
41+
@property
42+
def _tablename(cls):
43+
return cls.__name__.lower() if cls._table_name is None else cls._table_name
44+
1845
@classmethod
1946
def __get_primary_key(cls, primary_key_value: Any):
2047
"""
21-
Returns the primary key value concatenated to the table name for uniqueness
48+
Uses _table_name, _table_refix, and _redis_separator from the model to build our primary key.
49+
50+
_table_name defaults to the name of the model class if it is not set
51+
_redis_separator defualts to : if it is not set
52+
_prefix defaults to nothing if it is not set
53+
54+
The key is contructed as {_prefix}{_redis_separator}{_table_name}{_redis_separator}{primary_key_value}
55+
So a model named ThisModel with a primary key of id, by default would result in a key of thismodel:id
2256
"""
23-
table_name = (
24-
cls.__name__.lower() if cls._table_name is None else cls._table_name
25-
)
26-
return f"{table_name}_%&_{primary_key_value}"
57+
prefix = f"{cls._prefix}{cls._separator}" if cls._prefix != "" else ""
58+
return f"{prefix}{cls._tablename}{cls._separator}{primary_key_value}"
2759

2860
@classmethod
2961
def get_table_index_key(cls):
3062
"""Returns the key in which the primary keys of the given table have been saved"""
31-
table_name = (
32-
cls.__name__.lower() if cls._table_name is None else cls._table_name
33-
)
34-
return f"{table_name}__index"
63+
return f"{cls._tablename}{cls._separator}__index"
3564

3665
@classmethod
3766
async def _ids_to_primary_keys(

test/conftest.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import pytest
2+
import pytest_asyncio
23
import redislite
34
from pydantic_aioredis.config import RedisConfig
45
from pydantic_aioredis.model import Model
56
from pydantic_aioredis.store import Store
67

78

8-
@pytest.fixture()
9+
@pytest_asyncio.fixture()
910
def redis_server(unused_tcp_port):
1011
"""Sets up a fake redis server we can use for tests"""
11-
instance = redislite.Redis(serverconfig={"port": unused_tcp_port})
12-
yield unused_tcp_port
13-
instance.close()
12+
try:
13+
instance = redislite.Redis(serverconfig={"port": unused_tcp_port})
14+
yield unused_tcp_port
15+
finally:
16+
instance.close()
17+
instance.shutdown()

test/ext/FastAPI/test_ext_fastapi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import List
22

33
import pytest
4+
import pytest_asyncio
45
from fastapi import FastAPI
56
from httpx import AsyncClient
67
from pydantic_aioredis.config import RedisConfig
@@ -13,7 +14,7 @@ class Model(FastAPIModel):
1314
name: str
1415

1516

16-
@pytest.fixture()
17+
@pytest_asyncio.fixture()
1718
async def test_app(redis_server):
1819
store = Store(
1920
name="sample",

test/ext/FastAPI/test_ext_fastapi_crudrouter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import List
22

33
import pytest
4+
import pytest_asyncio
45
from fastapi import FastAPI
56
from httpx import AsyncClient
67
from pydantic_aioredis import Model as PAModel
@@ -15,7 +16,7 @@ class Model(PAModel):
1516
value: int
1617

1718

18-
@pytest.fixture()
19+
@pytest_asyncio.fixture()
1920
async def test_app(redis_server):
2021
store = Store(
2122
name="sample",
@@ -31,7 +32,7 @@ async def test_app(redis_server):
3132
yield store, app, Model
3233

3334

34-
@pytest.fixture()
35+
@pytest_asyncio.fixture()
3536
def test_models():
3637
return [Model(name=f"test{i}", value=i) for i in range(1, 10)]
3738

test/test_pydantic_aioredis.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from uuid import uuid4
1212

1313
import pytest
14+
import pytest_asyncio
1415
from pydantic_aioredis.config import RedisConfig
1516
from pydantic_aioredis.model import Model
1617
from pydantic_aioredis.store import Store
@@ -85,7 +86,7 @@ class ModelWithIP(Model):
8586
]
8687

8788

88-
@pytest.fixture()
89+
@pytest_asyncio.fixture()
8990
async def redis_store(redis_server):
9091
"""Sets up a redis store using the redis_server fixture and adds the book model to it"""
9192
store = Store(
@@ -150,10 +151,10 @@ def test_store_model(redis_store):
150151
async def test_bulk_insert(store, models, model_class):
151152
"""Providing a list of Model instances to the insert method inserts the records in redis"""
152153
keys = [
153-
f"{type(model).__name__.lower()}_%&_{getattr(model, type(model)._primary_key_field)}"
154+
f"{type(model).__name__.lower()}:{getattr(model, type(model)._primary_key_field)}"
154155
for model in models
155156
]
156-
# keys = [f"book_%&_{book.title}" for book in models]
157+
# keys = [f"book:{book.title}" for book in models]
157158
await store.redis_store.delete(*keys)
158159

159160
for key in keys:
@@ -179,7 +180,7 @@ async def test_insert_single(store, models, model_class):
179180
"""
180181
Providing a single Model instance
181182
"""
182-
key = f"{type(models[0]).__name__.lower()}_%&_{getattr(models[0], type(models[0])._primary_key_field)}"
183+
key = f"{type(models[0]).__name__.lower()}:{getattr(models[0], type(models[0])._primary_key_field)}"
183184
model = await store.redis_store.hgetall(name=key)
184185
assert model == {}
185186

@@ -196,7 +197,7 @@ async def test_insert_single_lifespan(store, models, model_class):
196197
"""
197198
Providing a single Model instance with a lifespam
198199
"""
199-
key = f"{type(models[0]).__name__.lower()}_%&_{getattr(models[0], type(models[0])._primary_key_field)}"
200+
key = f"{type(models[0]).__name__.lower()}:{getattr(models[0], type(models[0])._primary_key_field)}"
200201
model = await store.redis_store.hgetall(name=key)
201202
assert model == {}
202203

@@ -317,7 +318,7 @@ async def test_update(redis_store):
317318
await Book.insert(books)
318319
title = books[0].title
319320
new_author = "John Doe"
320-
key = f"book_%&_{title}"
321+
key = f"book:{title}"
321322
old_book_data = await redis_store.redis_store.hgetall(name=key)
322323
old_book = Book(**Book.deserialize_partially(old_book_data))
323324
assert old_book == books[0]
@@ -355,8 +356,8 @@ async def test_delete_multiple(redis_store):
355356
ids_to_delete = [book.title for book in books_to_delete]
356357
ids_to_leave_intact = [book.title for book in books_left_in_db]
357358

358-
keys_to_delete = [f"book_%&_{_id}" for _id in ids_to_delete]
359-
keys_to_leave_intact = [f"book_%&_{_id}" for _id in ids_to_leave_intact]
359+
keys_to_delete = [f"book:{_id}" for _id in ids_to_delete]
360+
keys_to_leave_intact = [f"book:{_id}" for _id in ids_to_leave_intact]
360361

361362
await Book.delete(ids=ids_to_delete)
362363

0 commit comments

Comments
 (0)