Skip to content

Commit ba28a79

Browse files
committed
merging
2 parents 6f716f6 + 75fa446 commit ba28a79

File tree

5 files changed

+140
-2
lines changed

5 files changed

+140
-2
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ sync: $(INSTALL_STAMP)
5353
lint: $(INSTALL_STAMP) dist
5454
$(POETRY) run isort --profile=black --lines-after-imports=2 ./tests/ $(NAME) $(SYNC_NAME)
5555
$(POETRY) run black ./tests/ $(NAME)
56-
$(POETRY) run flake8 --ignore=W503,E501,F401,E731 ./tests/ $(NAME) $(SYNC_NAME)
56+
$(POETRY) run flake8 --ignore=W503,E501,F401,E731,E712 ./tests/ $(NAME) $(SYNC_NAME)
5757
$(POETRY) run mypy ./tests/ $(NAME) $(SYNC_NAME) --ignore-missing-imports --exclude migrate.py --exclude _compat\.py$
5858
$(POETRY) run bandit -r $(NAME) $(SYNC_NAME) -s B608
5959

aredis_om/model/model.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def get_outer_type(field):
8383
field.annotation
8484
):
8585
return field.annotation
86+
elif not hasattr(field.annotation, "__args__"):
87+
return None
8688
else:
8789
return field.annotation.__args__[0]
8890

@@ -116,6 +118,8 @@ class Operators(Enum):
116118
STARTSWITH = 14
117119
ENDSWITH = 15
118120
CONTAINS = 16
121+
TRUE = 17
122+
FALSE = 18
119123

120124
def __str__(self):
121125
return str(self.name)
@@ -582,6 +586,8 @@ def resolve_field_type(
582586
"Only lists and tuples are supported for multi-value fields. "
583587
f"Docs: {ERRORS_URL}#E4"
584588
)
589+
elif field_type is bool:
590+
return RediSearchFieldTypes.TAG
585591
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
586592
# Index numeric Python types as NUMERIC fields, so we can support
587593
# range queries.
@@ -676,7 +682,11 @@ def resolve_value(
676682
separator_char,
677683
)
678684
return ""
679-
if isinstance(value, int):
685+
if isinstance(value, bool):
686+
result = "@{field_name}:{{{value}}}".format(
687+
field_name=field_name, value=value
688+
)
689+
elif isinstance(value, int):
680690
# This if will hit only if the field is a primary key of type int
681691
result = f"@{field_name}:[{value} {value}]"
682692
elif separator_char in value:
@@ -1628,6 +1638,11 @@ def check(self):
16281638
*_, validation_error = validate_model(self.__class__, self.__dict__)
16291639
if validation_error:
16301640
raise validation_error
1641+
else:
1642+
from pydantic import TypeAdapter
1643+
1644+
adapter = TypeAdapter(self.__class__)
1645+
adapter.validate_python(self.__dict__)
16311646

16321647

16331648
class HashModel(RedisModel, abc.ABC):
@@ -1817,6 +1832,8 @@ def schema_for_type(cls, name, typ: Any, field_info: PydanticFieldInfo):
18171832
return ""
18181833
embedded_cls = embedded_cls[0]
18191834
schema = cls.schema_for_type(name, embedded_cls, field_info)
1835+
elif typ is bool:
1836+
schema = f"{name} TAG"
18201837
elif any(issubclass(typ, t) for t in NUMERIC_TYPES):
18211838
vector_options: Optional[VectorFieldOptions] = getattr(
18221839
field_info, "vector_options", None
@@ -1946,6 +1963,9 @@ def schema_for_fields(cls):
19461963

19471964
for name, field in fields.items():
19481965
_type = get_outer_type(field)
1966+
if _type is None:
1967+
continue
1968+
19491969
if (
19501970
not isinstance(field, FieldInfo)
19511971
and hasattr(field, "metadata")
@@ -2124,6 +2144,8 @@ def schema_for_type(
21242144
raise sortable_tag_error
21252145
if case_sensitive is True:
21262146
schema += " CASESENSITIVE"
2147+
elif typ is bool:
2148+
schema = f"{path} AS {index_field_name} TAG"
21272149
elif any(issubclass(typ, t) for t in NUMERIC_TYPES):
21282150
schema = f"{path} AS {index_field_name} NUMERIC"
21292151
elif issubclass(typ, str):

docs/getting_started.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,27 @@ Customer.find((Customer.last_name == "Brookins") | (
702702
) & (Customer.last_name == "Smith")).all()
703703
```
704704

705+
### Saving and querying Boolean values
706+
707+
For historical reasons, saving and querying Boolean values is not supported in `HashModels`, however in JSON models,
708+
you may store and query Boolean values using the `==` syntax:
709+
710+
```python
711+
from redis_om import (
712+
Field,
713+
JsonModel,
714+
Migrator
715+
)
716+
717+
class Demo(JsonModel):
718+
b: bool = Field(index=True)
719+
720+
Migrator().run()
721+
d = Demo(b=True)
722+
d.save()
723+
res = Demo.find(Demo.b == True)
724+
```
725+
705726
## Calling Other Redis Commands
706727

707728
Sometimes you'll need to run a Redis command directly. Redis OM supports this through the `db` method on your model's class. This returns a connected Redis client instance which exposes a function named for each Redis command. For example, let's perform some basic set operations:

tests/test_hash_model.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,3 +896,21 @@ class ModelWithStringDefault(HashModel):
896896
await b.save()
897897
res = await ModelWithStringDefault.find(ModelWithStringDefault.pk == b.pk).first()
898898
assert res.test == "None"
899+
900+
901+
async def test_update_validation():
902+
class TestUpdate(HashModel):
903+
name: str
904+
age: int
905+
906+
await Migrator().run()
907+
t = TestUpdate(name="steve", age=34)
908+
await t.save()
909+
update_dict = dict()
910+
update_dict["age"] = "cat"
911+
912+
with pytest.raises(ValidationError):
913+
await t.update(**update_dict)
914+
915+
rematerialized = await TestUpdate.find(TestUpdate.pk == t.pk).first()
916+
assert rematerialized.age == 34

tests/test_json_model.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,3 +992,80 @@ class ModelWithStringDefault(JsonModel):
992992
await b.save()
993993
res = await ModelWithStringDefault.find(ModelWithStringDefault.pk == b.pk).first()
994994
assert res.test == "None"
995+
996+
997+
async def test_update_validation():
998+
999+
class Embedded(EmbeddedJsonModel):
1000+
price: float
1001+
name: str = Field(index=True)
1002+
1003+
class TestUpdatesClass(JsonModel):
1004+
name: str
1005+
age: int
1006+
embedded: Embedded
1007+
1008+
await Migrator().run()
1009+
embedded = Embedded(price=3.14, name="foo")
1010+
t = TestUpdatesClass(name="str", age=42, embedded=embedded)
1011+
await t.save()
1012+
1013+
update_dict = dict()
1014+
update_dict["age"] = "foo"
1015+
with pytest.raises(ValidationError):
1016+
await t.update(**update_dict)
1017+
1018+
t.age = 42
1019+
update_dict.clear()
1020+
update_dict["embedded"] = "hello"
1021+
with pytest.raises(ValidationError):
1022+
await t.update(**update_dict)
1023+
1024+
rematerialized = await TestUpdatesClass.find(TestUpdatesClass.pk == t.pk).first()
1025+
assert rematerialized.age == 42
1026+
1027+
1028+
async def test_model_with_dict():
1029+
class EmbeddedJsonModelWithDict(EmbeddedJsonModel):
1030+
dict: Dict
1031+
1032+
class ModelWithDict(JsonModel):
1033+
embedded_model: EmbeddedJsonModelWithDict
1034+
info: Dict
1035+
1036+
await Migrator().run()
1037+
d = dict()
1038+
inner_dict = dict()
1039+
d["foo"] = "bar"
1040+
inner_dict["bar"] = "foo"
1041+
embedded_model = EmbeddedJsonModelWithDict(dict=inner_dict)
1042+
item = ModelWithDict(info=d, embedded_model=embedded_model)
1043+
await item.save()
1044+
1045+
rematerialized = await ModelWithDict.find(ModelWithDict.pk == item.pk).first()
1046+
assert rematerialized.pk == item.pk
1047+
assert rematerialized.info["foo"] == "bar"
1048+
assert rematerialized.embedded_model.dict["bar"] == "foo"
1049+
1050+
1051+
@py_test_mark_asyncio
1052+
async def test_boolean():
1053+
class Example(JsonModel):
1054+
b: bool = Field(index=True)
1055+
d: datetime.date = Field(index=True)
1056+
name: str = Field(index=True)
1057+
1058+
await Migrator().run()
1059+
1060+
ex = Example(b=True, name="steve", d=datetime.date.today())
1061+
exFalse = Example(b=False, name="foo", d=datetime.date.today())
1062+
await ex.save()
1063+
await exFalse.save()
1064+
res = await Example.find(Example.b == True).first()
1065+
assert res.name == "steve"
1066+
1067+
res = await Example.find(Example.b == False).first()
1068+
assert res.name == "foo"
1069+
1070+
res = await Example.find(Example.d == ex.d and Example.b == True).first()
1071+
assert res.name == ex.name

0 commit comments

Comments
 (0)