2424 no_type_check ,
2525)
2626
27- import aioredis
28- from aioredis .client import Pipeline
27+ from more_itertools import ichunked
2928from pydantic import BaseModel , validator
3029from pydantic .fields import FieldInfo as PydanticFieldInfo
3130from pydantic .fields import ModelField , Undefined , UndefinedType
3534from typing_extensions import Protocol , get_args , get_origin
3635from ulid import ULID
3736
37+ from .. import redis
3838from ..checks import has_redis_json , has_redisearch
3939from ..connections import get_redis_connection
40- from ..unasync_util import ASYNC_MODE
40+ from ..util import ASYNC_MODE
4141from .encoders import jsonable_encoder
4242from .render_tree import render_tree
4343from .token_escaper import TokenEscaper
@@ -760,6 +760,9 @@ async def all(self, batch_size=DEFAULT_PAGE_SIZE):
760760 return await query .execute ()
761761 return await self .execute ()
762762
763+ async def page (self , offset = 0 , limit = 10 ):
764+ return await self .copy (offset = offset , limit = limit ).execute ()
765+
763766 def sort_by (self , * fields : str ):
764767 if not fields :
765768 return self
@@ -975,7 +978,7 @@ class BaseMeta(Protocol):
975978 global_key_prefix : str
976979 model_key_prefix : str
977980 primary_key_pattern : str
978- database : aioredis .Redis
981+ database : redis .Redis
979982 primary_key : PrimaryKey
980983 primary_key_creator_cls : Type [PrimaryKeyCreator ]
981984 index_name : str
@@ -994,7 +997,7 @@ class DefaultMeta:
994997 global_key_prefix : Optional [str ] = None
995998 model_key_prefix : Optional [str ] = None
996999 primary_key_pattern : Optional [str ] = None
997- database : Optional [aioredis .Redis ] = None
1000+ database : Optional [redis .Redis ] = None
9981001 primary_key : Optional [PrimaryKey ] = None
9991002 primary_key_creator_cls : Optional [Type [PrimaryKeyCreator ]] = None
10001003 index_name : Optional [str ] = None
@@ -1102,6 +1105,7 @@ class Config:
11021105 extra = "allow"
11031106
11041107 def __init__ (__pydantic_self__ , ** data : Any ) -> None :
1108+ data = {key : val for key , val in data .items () if val }
11051109 super ().__init__ (** data )
11061110 __pydantic_self__ .validate_primary_key ()
11071111
@@ -1115,9 +1119,17 @@ def key(self):
11151119 return self .make_primary_key (pk )
11161120
11171121 @classmethod
1118- async def delete (cls , pk : Any ) -> int :
1122+ async def _delete (cls , db , * pks ):
1123+ return await db .delete (* pks )
1124+
1125+ @classmethod
1126+ async def delete (
1127+ cls , pk : Any , pipeline : Optional [redis .client .Pipeline ] = None
1128+ ) -> int :
11191129 """Delete data at this key."""
1120- return await cls .db ().delete (cls .make_primary_key (pk ))
1130+ db = cls ._get_db (pipeline )
1131+
1132+ return await cls ._delete (db , cls .make_primary_key (pk ))
11211133
11221134 @classmethod
11231135 async def get (cls , pk : Any ) -> "RedisModel" :
@@ -1127,14 +1139,15 @@ async def update(self, **field_values):
11271139 """Update this model instance with the specified key-value pairs."""
11281140 raise NotImplementedError
11291141
1130- async def save (self , pipeline : Optional [Pipeline ] = None ) -> "RedisModel" :
1142+ async def save (
1143+ self , pipeline : Optional [redis .client .Pipeline ] = None
1144+ ) -> "RedisModel" :
11311145 raise NotImplementedError
11321146
1133- async def expire (self , num_seconds : int , pipeline : Optional [Pipeline ] = None ):
1134- if pipeline is None :
1135- db = self .db ()
1136- else :
1137- db = pipeline
1147+ async def expire (
1148+ self , num_seconds : int , pipeline : Optional [redis .client .Pipeline ] = None
1149+ ):
1150+ db = self ._get_db (pipeline )
11381151
11391152 # TODO: Wrap any Redis response errors in a custom exception?
11401153 await db .expire (self .make_primary_key (self .pk ), num_seconds )
@@ -1223,19 +1236,10 @@ def get_annotations(cls):
12231236 async def add (
12241237 cls ,
12251238 models : Sequence ["RedisModel" ],
1226- pipeline : Optional [Pipeline ] = None ,
1239+ pipeline : Optional [redis . client . Pipeline ] = None ,
12271240 pipeline_verifier : Callable [..., Any ] = verify_pipeline_response ,
12281241 ) -> Sequence ["RedisModel" ]:
1229- if pipeline is None :
1230- # By default, send commands in a pipeline. Saving each model will
1231- # be atomic, but Redis may process other commands in between
1232- # these saves.
1233- db = cls .db ().pipeline (transaction = False )
1234- else :
1235- # If the user gave us a pipeline, add our commands to that. The user
1236- # will be responsible for executing the pipeline after they've accumulated
1237- # the commands they want to send.
1238- db = pipeline
1242+ db = cls ._get_db (pipeline , bulk = True )
12391243
12401244 for model in models :
12411245 # save() just returns the model, we don't need that here.
@@ -1249,6 +1253,31 @@ async def add(
12491253
12501254 return models
12511255
1256+ @classmethod
1257+ def _get_db (
1258+ self , pipeline : Optional [redis .client .Pipeline ] = None , bulk : bool = False
1259+ ):
1260+ if pipeline is not None :
1261+ return pipeline
1262+ elif bulk :
1263+ return self .db ().pipeline (transaction = False )
1264+ else :
1265+ return self .db ()
1266+
1267+ @classmethod
1268+ async def delete_many (
1269+ cls ,
1270+ models : Sequence ["RedisModel" ],
1271+ pipeline : Optional [redis .client .Pipeline ] = None ,
1272+ ) -> int :
1273+ db = cls ._get_db (pipeline )
1274+
1275+ for chunk in ichunked (models , 100 ):
1276+ pks = [cls .make_primary_key (model .pk ) for model in chunk ]
1277+ await cls ._delete (db , * pks )
1278+
1279+ return len (models )
1280+
12521281 @classmethod
12531282 def redisearch_schema (cls ):
12541283 raise NotImplementedError
@@ -1283,17 +1312,13 @@ def __init_subclass__(cls, **kwargs):
12831312 f"HashModels cannot index dataclass fields. Field: { name } "
12841313 )
12851314
1286- def dict (self ) -> Dict [str , Any ]:
1287- # restore none values
1288- return dict (self )
1289-
1290- async def save (self , pipeline : Optional [Pipeline ] = None ) -> "HashModel" :
1315+ async def save (self , pipeline : Optional [redis .client .Pipeline ] = None ) -> "HashModel" :
12911316 self .check ()
12921317 if pipeline is None :
12931318 db = self .db ()
12941319 else :
12951320 db = pipeline
1296- document = jsonable_encoder ({ key : val if val else "0" for key , val in self .dict (). items ()} )
1321+ document = jsonable_encoder (self .dict ())
12971322 # TODO: Wrap any Redis response errors in a custom exception?
12981323 await db .hset (self .key (), mapping = document )
12991324 return self
@@ -1461,12 +1486,12 @@ def __init__(self, *args, **kwargs):
14611486 )
14621487 super ().__init__ (* args , ** kwargs )
14631488
1464- async def save (self , pipeline : Optional [Pipeline ] = None ) -> "JsonModel" :
1489+ async def save (
1490+ self , pipeline : Optional [redis .client .Pipeline ] = None
1491+ ) -> "JsonModel" :
14651492 self .check ()
1466- if pipeline is None :
1467- db = self .db ()
1468- else :
1469- db = pipeline
1493+ db = self ._get_db (pipeline )
1494+
14701495 # TODO: Wrap response errors in a custom exception?
14711496 await db .execute_command ("JSON.SET" , self .key (), "." , self .json ())
14721497 return self
0 commit comments