Skip to content

Commit efa2603

Browse files
committed
Add async pydantic redis
1 parent a65cd68 commit efa2603

File tree

9 files changed

+686
-11
lines changed

9 files changed

+686
-11
lines changed

pydantic_redis/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""Entry point for redisy"""
22

3-
from pydantic_redis.shared.config import RedisConfig
4-
from pydantic_redis.syncio.model import Model
5-
from pydantic_redis.syncio.store import Store
3+
from pydantic_redis.syncio import Store, Model, RedisConfig
4+
import pydantic_redis.asyncio
65

7-
__all__ = [Store, RedisConfig, Model]
6+
__all__ = [Store, RedisConfig, Model, asyncio]
87

98
__version__ = "0.2.0"

pydantic_redis/asyncio/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Package containing the async version of pydantic_redis"""
2+
3+
from .model import Model
4+
from .store import Store
5+
from ..shared.config import RedisConfig
6+
7+
__all__ = [Model, Store, RedisConfig]

pydantic_redis/asyncio/model.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Module containing the model classes"""
2+
from typing import Optional, List, Any, Union, Dict, Tuple, Type
3+
4+
import redis.asyncio
5+
6+
from pydantic_redis.shared.model import AbstractModel
7+
from pydantic_redis.shared.model.insert_utils import insert_on_pipeline
8+
from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
9+
from pydantic_redis.shared.model.select_utils import (
10+
select_all_fields_all_ids,
11+
select_all_fields_some_ids,
12+
select_some_fields_all_ids,
13+
select_some_fields_some_ids,
14+
parse_select_response,
15+
)
16+
17+
from .store import Store
18+
19+
20+
class Model(AbstractModel):
21+
"""
22+
The section in the store that saves rows of the same kind
23+
"""
24+
25+
_store: Store
26+
27+
@classmethod
28+
async def insert(
29+
cls,
30+
data: Union[List[AbstractModel], AbstractModel],
31+
life_span_seconds: Optional[float] = None,
32+
):
33+
"""
34+
Inserts a given row or sets of rows into the table
35+
"""
36+
store = cls.get_store()
37+
life_span = (
38+
life_span_seconds
39+
if life_span_seconds is not None
40+
else store.life_span_in_seconds
41+
)
42+
43+
async with store.redis_store.pipeline(transaction=True) as pipeline:
44+
data_list = []
45+
46+
if isinstance(data, list):
47+
data_list = data
48+
elif isinstance(data, AbstractModel):
49+
data_list = [data]
50+
51+
for record in data_list:
52+
insert_on_pipeline(
53+
model=cls,
54+
_id=None,
55+
pipeline=pipeline,
56+
record=record,
57+
life_span=life_span,
58+
)
59+
60+
return await pipeline.execute()
61+
62+
@classmethod
63+
async def update(
64+
cls, _id: Any, data: Dict[str, Any], life_span_seconds: Optional[float] = None
65+
):
66+
"""
67+
Updates a given row or sets of rows in the table
68+
"""
69+
store = cls.get_store()
70+
life_span = (
71+
life_span_seconds
72+
if life_span_seconds is not None
73+
else store.life_span_in_seconds
74+
)
75+
async with store.redis_store.pipeline(transaction=True) as pipeline:
76+
if isinstance(data, dict):
77+
insert_on_pipeline(
78+
model=cls,
79+
_id=_id,
80+
pipeline=pipeline,
81+
record=data,
82+
life_span=life_span,
83+
)
84+
85+
return await pipeline.execute()
86+
87+
@classmethod
88+
async def delete(cls, ids: Union[Any, List[Any]]):
89+
"""
90+
deletes a given row or sets of rows in the table
91+
"""
92+
store = cls.get_store()
93+
94+
async with store.redis_store.pipeline() as pipeline:
95+
primary_keys = []
96+
97+
if isinstance(ids, list):
98+
primary_keys = ids
99+
elif ids is not None:
100+
primary_keys = [ids]
101+
102+
names = [
103+
get_primary_key(model=cls, primary_key_value=primary_key_value)
104+
for primary_key_value in primary_keys
105+
]
106+
pipeline.delete(*names)
107+
# remove the primary keys from the index
108+
table_index_key = get_table_index_key(model=cls)
109+
pipeline.srem(table_index_key, *names)
110+
return await pipeline.execute()
111+
112+
@classmethod
113+
async def select(
114+
cls,
115+
columns: Optional[List[str]] = None,
116+
ids: Optional[List[Any]] = None,
117+
**kwargs,
118+
):
119+
"""
120+
Selects given rows or sets of rows in the table
121+
"""
122+
if columns is None and ids is None:
123+
response = await select_all_fields_all_ids(model=cls)
124+
125+
elif columns is None and isinstance(ids, list):
126+
response = await select_all_fields_some_ids(model=cls, ids=ids)
127+
128+
elif isinstance(columns, list) and ids is None:
129+
response = await select_some_fields_all_ids(model=cls, fields=columns)
130+
131+
elif isinstance(columns, list) and isinstance(ids, list):
132+
response = await select_some_fields_some_ids(
133+
model=cls, fields=columns, ids=ids
134+
)
135+
136+
else:
137+
raise ValueError(
138+
f"columns {columns}, ids: {ids} should be either None or lists"
139+
)
140+
141+
return parse_select_response(
142+
model=cls, response=response, as_models=(columns is None)
143+
)

pydantic_redis/asyncio/store.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Module containing the store class for async io"""
2+
from typing import Dict, Type, TYPE_CHECKING
3+
4+
from redis import asyncio as redis
5+
6+
from ..shared.store import AbstractStore
7+
8+
if TYPE_CHECKING:
9+
from .model import Model
10+
11+
12+
class Store(AbstractStore):
13+
"""
14+
A store that allows a declarative way of querying for data in redis
15+
"""
16+
17+
models: Dict[str, Type["Model"]] = {}
18+
19+
def _connect_to_redis(self) -> redis.Redis:
20+
"""Connects the store to redis, returning a proper connection"""
21+
return redis.from_url(
22+
self.redis_config.redis_url,
23+
encoding=self.redis_config.encoding,
24+
decode_responses=True,
25+
)

pydantic_redis/syncio/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
"""Package containing the synchronous and thus default version of pydantic_redis"""
2+
from .model import Model
3+
from .store import Store
4+
from ..shared.config import RedisConfig
5+
6+
__all__ = [Model, Store, RedisConfig]

pydantic_redis/syncio/model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ def delete(cls, ids: Union[Any, List[Any]]):
8787
"""
8888
deletes a given row or sets of rows in the table
8989
"""
90-
with cls._store.redis_store.pipeline() as pipeline:
90+
store = cls.get_store()
91+
with store.redis_store.pipeline() as pipeline:
9192
primary_keys = []
9293

9394
if isinstance(ids, list):

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ twine==3.8.0
1010
black==22.8.0
1111
pre-commit
1212
build
13+
pytest-asyncio

test/conftest.py

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
33
from typing import Tuple, List, Optional
44

55
import pytest
6+
import pytest_asyncio
67
import redislite
78
from pytest_lazyfixture import lazy_fixture
89

9-
from pydantic_redis import Store, RedisConfig, Model
10+
from pydantic_redis import syncio as syn, asyncio as asy
1011

1112

12-
class Author(Model):
13+
class Author(syn.Model):
1314
_primary_key_field: str = "name"
1415
name: str
1516
active_years: Tuple[int, int]
1617

1718

18-
class Book(Model):
19+
class AsyncAuthor(asy.Model):
20+
_primary_key_field: str = "name"
21+
name: str
22+
active_years: Tuple[int, int]
23+
24+
25+
class Book(syn.Model):
1926
_primary_key_field: str = "title"
2027
title: str
2128
author: Author
@@ -25,7 +32,17 @@ class Book(Model):
2532
in_stock: bool = True
2633

2734

28-
class Library(Model):
35+
class AsyncBook(asy.Model):
36+
_primary_key_field: str = "title"
37+
title: str
38+
author: AsyncAuthor
39+
rating: float
40+
published_on: date
41+
tags: List[str] = []
42+
in_stock: bool = True
43+
44+
45+
class Library(syn.Model):
2946
# the _primary_key_field is mandatory
3047
_primary_key_field: str = "name"
3148
name: str
@@ -36,11 +53,27 @@ class Library(Model):
3653
new: Tuple[Book, Author, Book, int] = None
3754

3855

56+
class AsyncLibrary(asy.Model):
57+
# the _primary_key_field is mandatory
58+
_primary_key_field: str = "name"
59+
name: str
60+
address: str
61+
books: List[AsyncBook] = None
62+
lost: Optional[List[AsyncBook]] = None
63+
popular: Optional[Tuple[AsyncBook, AsyncBook]] = None
64+
new: Tuple[AsyncBook, AsyncAuthor, AsyncBook, int] = None
65+
66+
3967
authors = {
4068
"charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
4169
"jane": Author(name="Jane Austen", active_years=(1580, 1640)),
4270
}
4371

72+
async_authors = {
73+
"charles": AsyncAuthor(name="Charles Dickens", active_years=(1220, 1280)),
74+
"jane": AsyncAuthor(name="Jane Austen", active_years=(1580, 1640)),
75+
}
76+
4477
books = [
4578
Book(
4679
title="Oliver Twist",
@@ -74,6 +107,40 @@ class Library(Model):
74107
),
75108
]
76109

110+
async_books = [
111+
AsyncBook(
112+
title="Oliver Twist",
113+
author=authors["charles"],
114+
published_on=date(year=1215, month=4, day=4),
115+
in_stock=False,
116+
rating=2,
117+
tags=["Classic"],
118+
),
119+
AsyncBook(
120+
title="Great Expectations",
121+
author=authors["charles"],
122+
published_on=date(year=1220, month=4, day=4),
123+
rating=5,
124+
tags=["Classic"],
125+
),
126+
AsyncBook(
127+
title="Jane Eyre",
128+
author=authors["charles"],
129+
published_on=date(year=1225, month=6, day=4),
130+
in_stock=False,
131+
rating=3.4,
132+
tags=["Classic", "Romance"],
133+
),
134+
AsyncBook(
135+
title="Wuthering Heights",
136+
author=authors["jane"],
137+
published_on=date(year=1600, month=4, day=4),
138+
rating=4.0,
139+
tags=["Classic", "Romance"],
140+
),
141+
]
142+
143+
# sync fixtures
77144
redis_store_fixture = [(lazy_fixture("redis_store"))]
78145
books_fixture = [(lazy_fixture("redis_store"), book) for book in books]
79146
update_books_fixture = [
@@ -88,6 +155,23 @@ class Library(Model):
88155
(lazy_fixture("redis_store"), book.title) for book in books[-1:]
89156
]
90157

158+
# async fixtures
159+
async_redis_store_fixture = [(lazy_fixture("async_redis_store"))]
160+
async_books_fixture = [
161+
(lazy_fixture("async_redis_store"), book) for book in async_books
162+
]
163+
async_update_books_fixture = [
164+
(
165+
lazy_fixture("async_redis_store"),
166+
book.title,
167+
{"author": authors["jane"], "in_stock": not book.in_stock},
168+
)
169+
for book in async_books[-1:]
170+
]
171+
async_delete_books_fixture = [
172+
(lazy_fixture("async_redis_store"), book.title) for book in async_books[-1:]
173+
]
174+
91175

92176
@pytest.fixture()
93177
def unused_tcp_port():
@@ -110,13 +194,28 @@ def redis_server(unused_tcp_port):
110194
@pytest.fixture()
111195
def redis_store(redis_server):
112196
"""Sets up a redis store using the redis_server fixture and adds the book model to it"""
113-
store = Store(
197+
store = syn.Store(
114198
name="sample",
115-
redis_config=RedisConfig(port=redis_server, db=1),
199+
redis_config=syn.RedisConfig(port=redis_server, db=1),
116200
life_span_in_seconds=3600,
117201
)
118202
store.register_model(Book)
119203
store.register_model(Author)
120204
store.register_model(Library)
121205
yield store
122206
store.redis_store.flushall()
207+
208+
209+
@pytest_asyncio.fixture
210+
async def async_redis_store(redis_server):
211+
"""Sets up a redis store using the redis_server fixture and adds the book model to it"""
212+
store = asy.Store(
213+
name="sample",
214+
redis_config=syn.RedisConfig(port=redis_server, db=1),
215+
life_span_in_seconds=3600,
216+
)
217+
store.register_model(AsyncBook)
218+
store.register_model(AsyncAuthor)
219+
store.register_model(AsyncLibrary)
220+
yield store
221+
await store.redis_store.flushall()

0 commit comments

Comments
 (0)