11
11
AbstractSet ,
12
12
Any ,
13
13
Callable ,
14
+ ClassVar ,
14
15
Dict ,
15
16
List ,
17
+ Literal ,
16
18
Mapping ,
17
19
Optional ,
18
20
Sequence ,
32
34
from ulid import ULID
33
35
34
36
from .. import redis
35
- from .._compat import BaseModel
37
+ from .._compat import PYDANTIC_V2 , BaseModel
36
38
from .._compat import FieldInfo as PydanticFieldInfo
37
39
from .._compat import (
38
40
ModelField ,
@@ -140,10 +142,10 @@ def embedded(cls):
140
142
141
143
def is_supported_container_type (typ : Optional [type ]) -> bool :
142
144
# TODO: Wait, why don't we support indexing sets?
143
- if typ == list or typ == tuple :
145
+ if typ == list or typ == tuple or typ == Literal :
144
146
return True
145
147
unwrapped = get_origin (typ )
146
- return unwrapped == list or unwrapped == tuple
148
+ return unwrapped == list or unwrapped == tuple or unwrapped == Literal
147
149
148
150
149
151
def validate_model_fields (model : Type ["RedisModel" ], field_values : Dict [str , Any ]):
@@ -871,7 +873,9 @@ def resolve_redisearch_query(cls, expression: ExpressionOrNegated) -> str:
871
873
872
874
return result
873
875
874
- async def execute (self , exhaust_results = True , return_raw_result = False ):
876
+ async def execute (
877
+ self , exhaust_results = True , return_raw_result = False , return_query_args = False
878
+ ):
875
879
args : List [Union [str , bytes ]] = [
876
880
"FT.SEARCH" ,
877
881
self .model .Meta .index_name ,
@@ -896,6 +900,9 @@ async def execute(self, exhaust_results=True, return_raw_result=False):
896
900
if self .nocontent :
897
901
args .append ("NOCONTENT" )
898
902
903
+ if return_query_args :
904
+ return self .model .Meta .index_name , args
905
+
899
906
# Reset the cache if we're executing from offset 0.
900
907
if self .offset == 0 :
901
908
self ._model_cache .clear ()
@@ -929,6 +936,10 @@ async def execute(self, exhaust_results=True, return_raw_result=False):
929
936
self ._model_cache += _results
930
937
return self ._model_cache
931
938
939
+ async def get_query (self ):
940
+ query = self .copy ()
941
+ return await query .execute (return_query_args = True )
942
+
932
943
async def first (self ):
933
944
query = self .copy (offset = 0 , limit = 1 , sort_fields = self .sort_fields )
934
945
results = await query .execute (exhaust_results = False )
@@ -948,7 +959,9 @@ async def all(self, batch_size=DEFAULT_PAGE_SIZE):
948
959
return await self .execute ()
949
960
950
961
async def page (self , offset = 0 , limit = 10 ):
951
- return await self .copy (offset = offset , limit = limit ).execute ()
962
+ return await self .copy (offset = offset , limit = limit ).execute (
963
+ exhaust_results = False
964
+ )
952
965
953
966
def sort_by (self , * fields : str ):
954
967
if not fields :
@@ -1411,19 +1424,30 @@ def outer_type_or_annotation(field):
1411
1424
if not isinstance (field .annotation , type ):
1412
1425
raise AttributeError (f"could not extract outer type from field { field } " )
1413
1426
return field .annotation
1427
+ elif get_origin (field .annotation ) == Literal :
1428
+ return str
1414
1429
else :
1415
1430
return field .annotation .__args__ [0 ]
1416
1431
1417
1432
1418
1433
class RedisModel (BaseModel , abc .ABC , metaclass = ModelMeta ):
1419
1434
pk : Optional [str ] = Field (default = None , primary_key = True )
1435
+ ConfigDict : ClassVar
1420
1436
1421
1437
Meta = DefaultMeta
1422
1438
1423
- class Config :
1424
- orm_mode = True
1425
- arbitrary_types_allowed = True
1426
- extra = "allow"
1439
+ if PYDANTIC_V2 :
1440
+ from pydantic import ConfigDict
1441
+
1442
+ model_config = ConfigDict (
1443
+ from_attributes = True , arbitrary_types_allowed = True , extra = "allow"
1444
+ )
1445
+ else :
1446
+
1447
+ class Config :
1448
+ orm_mode = True
1449
+ arbitrary_types_allowed = True
1450
+ extra = "allow"
1427
1451
1428
1452
def __init__ (__pydantic_self__ , ** data : Any ) -> None :
1429
1453
__pydantic_self__ .validate_primary_key ()
@@ -1631,9 +1655,6 @@ def redisearch_schema(cls):
1631
1655
1632
1656
def check (self ):
1633
1657
"""Run all validations."""
1634
- from pydantic .version import VERSION as PYDANTIC_VERSION
1635
-
1636
- PYDANTIC_V2 = PYDANTIC_VERSION .startswith ("2." )
1637
1658
if not PYDANTIC_V2 :
1638
1659
* _ , validation_error = validate_model (self .__class__ , self .__dict__ )
1639
1660
if validation_error :
@@ -1655,8 +1676,8 @@ def __init_subclass__(cls, **kwargs):
1655
1676
for typ in (Set , Mapping , List ):
1656
1677
if isinstance (origin , type ) and issubclass (origin , typ ):
1657
1678
raise RedisModelError (
1658
- f"HashModels cannot index set, list,"
1659
- f" or mapping fields. Field: { name } "
1679
+ f"HashModels cannot index set, list, "
1680
+ f"or mapping fields. Field: { name } "
1660
1681
)
1661
1682
if isinstance (field_type , type ) and issubclass (field_type , RedisModel ):
1662
1683
raise RedisModelError (
@@ -1676,8 +1697,8 @@ def __init_subclass__(cls, **kwargs):
1676
1697
for typ in (Set , Mapping , List ):
1677
1698
if issubclass (origin , typ ):
1678
1699
raise RedisModelError (
1679
- f"HashModels cannot index set, list,"
1680
- f" or mapping fields. Field: { name } "
1700
+ f"HashModels cannot index set, list, "
1701
+ f"or mapping fields. Field: { name } "
1681
1702
)
1682
1703
1683
1704
if issubclass (outer_type , RedisModel ):
@@ -1982,7 +2003,9 @@ def schema_for_fields(cls):
1982
2003
if issubclass (_type , str ):
1983
2004
redisearch_field = f"$.{ name } AS { name } TAG SEPARATOR { SINGLE_VALUE_TAG_FIELD_SEPARATOR } "
1984
2005
else :
1985
- redisearch_field = cls .schema_for_type (name , _type , field_info )
2006
+ redisearch_field = cls .schema_for_type (
2007
+ json_path , name , "" , _type , field_info
2008
+ )
1986
2009
schema_parts .append (redisearch_field )
1987
2010
continue
1988
2011
schema_parts .append (
@@ -2046,21 +2069,33 @@ def schema_for_type(
2046
2069
# find any values marked as indexed.
2047
2070
if is_container_type and not is_vector :
2048
2071
field_type = get_origin (typ )
2049
- embedded_cls = get_args (typ )
2050
- if not embedded_cls :
2051
- log .warning (
2052
- "Model %s defined an empty list or tuple field: %s" , cls , name
2072
+ if field_type == Literal :
2073
+ path = f"{ json_path } .{ name } "
2074
+ return cls .schema_for_type (
2075
+ path ,
2076
+ name ,
2077
+ name_prefix ,
2078
+ str ,
2079
+ field_info ,
2080
+ parent_type = field_type ,
2081
+ )
2082
+ else :
2083
+ embedded_cls = get_args (typ )
2084
+ if not embedded_cls :
2085
+ log .warning (
2086
+ "Model %s defined an empty list or tuple field: %s" , cls , name
2087
+ )
2088
+ return ""
2089
+ path = f"{ json_path } .{ name } [*]"
2090
+ embedded_cls = embedded_cls [0 ]
2091
+ return cls .schema_for_type (
2092
+ path ,
2093
+ name ,
2094
+ name_prefix ,
2095
+ embedded_cls ,
2096
+ field_info ,
2097
+ parent_type = field_type ,
2053
2098
)
2054
- return ""
2055
- embedded_cls = embedded_cls [0 ]
2056
- return cls .schema_for_type (
2057
- f"{ json_path } .{ name } [*]" ,
2058
- name ,
2059
- name_prefix ,
2060
- embedded_cls ,
2061
- field_info ,
2062
- parent_type = field_type ,
2063
- )
2064
2099
elif field_is_model :
2065
2100
name_prefix = f"{ name_prefix } _{ name } " if name_prefix else name
2066
2101
sub_fields = []
0 commit comments