Document some possible error messages
This commit is contained in:
parent
60fbd09775
commit
e4e3583006
5 changed files with 262 additions and 25 deletions
|
@ -69,14 +69,6 @@ def jsonable_encoder(
|
||||||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||||
exclude = set(exclude)
|
exclude = set(exclude)
|
||||||
|
|
||||||
if custom_encoder:
|
|
||||||
if type(obj) in custom_encoder:
|
|
||||||
return custom_encoder[type(obj)](obj)
|
|
||||||
else:
|
|
||||||
for encoder_type, encoder in custom_encoder.items():
|
|
||||||
if isinstance(obj, encoder_type):
|
|
||||||
return encoder(obj)
|
|
||||||
|
|
||||||
if isinstance(obj, BaseModel):
|
if isinstance(obj, BaseModel):
|
||||||
encoder = getattr(obj.__config__, "json_encoders", {})
|
encoder = getattr(obj.__config__, "json_encoders", {})
|
||||||
if custom_encoder:
|
if custom_encoder:
|
||||||
|
@ -154,9 +146,13 @@ def jsonable_encoder(
|
||||||
)
|
)
|
||||||
return encoded_list
|
return encoded_list
|
||||||
|
|
||||||
# This function originally called custom encoders here,
|
if custom_encoder:
|
||||||
# which meant we couldn't override the encoder for many
|
if type(obj) in custom_encoder:
|
||||||
# types hard-coded into this function (lists, etc.).
|
return custom_encoder[type(obj)](obj)
|
||||||
|
else:
|
||||||
|
for encoder_type, encoder in custom_encoder.items():
|
||||||
|
if isinstance(obj, encoder_type):
|
||||||
|
return encoder(obj)
|
||||||
|
|
||||||
if type(obj) in ENCODERS_BY_TYPE:
|
if type(obj) in ENCODERS_BY_TYPE:
|
||||||
return ENCODERS_BY_TYPE[type(obj)](obj)
|
return ENCODERS_BY_TYPE[type(obj)](obj)
|
||||||
|
|
|
@ -63,6 +63,8 @@ SINGLE_VALUE_TAG_FIELD_SEPARATOR = "|"
|
||||||
# multi-value field lookup, like a IN or NOT_IN.
|
# multi-value field lookup, like a IN or NOT_IN.
|
||||||
DEFAULT_REDISEARCH_FIELD_SEPARATOR = ","
|
DEFAULT_REDISEARCH_FIELD_SEPARATOR = ","
|
||||||
|
|
||||||
|
ERRORS_URL = "https://github.com/redis/redis-om-python/blob/main/docs/errors.md"
|
||||||
|
|
||||||
|
|
||||||
class RedisModelError(Exception):
|
class RedisModelError(Exception):
|
||||||
"""Raised when a problem exists in the definition of a RedisModel."""
|
"""Raised when a problem exists in the definition of a RedisModel."""
|
||||||
|
@ -288,6 +290,11 @@ class ExpressionProxy:
|
||||||
left=self.field, op=Operators.IN, right=other, parents=self.parents
|
left=self.field, op=Operators.IN, right=other, parents=self.parents
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __rshift__(self, other: Any) -> Expression:
|
||||||
|
return Expression(
|
||||||
|
left=self.field, op=Operators.NOT_IN, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if is_supported_container_type(self.field.outer_type_):
|
if is_supported_container_type(self.field.outer_type_):
|
||||||
embedded_cls = get_args(self.field.outer_type_)
|
embedded_cls = get_args(self.field.outer_type_)
|
||||||
|
@ -295,7 +302,7 @@ class ExpressionProxy:
|
||||||
raise QuerySyntaxError(
|
raise QuerySyntaxError(
|
||||||
"In order to query on a list field, you must define "
|
"In order to query on a list field, you must define "
|
||||||
"the contents of the list with a type annotation, like: "
|
"the contents of the list with a type annotation, like: "
|
||||||
"orders: List[Order]. Docs: TODO"
|
f"orders: List[Order]. Docs: {ERRORS_URL}#E1"
|
||||||
)
|
)
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
attr = getattr(embedded_cls, item)
|
attr = getattr(embedded_cls, item)
|
||||||
|
@ -419,7 +426,7 @@ class FindQuery:
|
||||||
if not getattr(field_proxy.field.field_info, "sortable", False):
|
if not getattr(field_proxy.field.field_info, "sortable", False):
|
||||||
raise QueryNotSupportedError(
|
raise QueryNotSupportedError(
|
||||||
f"You tried sort by {field_name}, but {self.model} does "
|
f"You tried sort by {field_name}, but {self.model} does "
|
||||||
"not define that field as sortable. See docs: XXX"
|
f"not define that field as sortable. Docs: {ERRORS_URL}#E2"
|
||||||
)
|
)
|
||||||
return sort_fields
|
return sort_fields
|
||||||
|
|
||||||
|
@ -433,7 +440,7 @@ class FindQuery:
|
||||||
raise QuerySyntaxError(
|
raise QuerySyntaxError(
|
||||||
f"You tried to do a full-text search on the field '{field.name}', "
|
f"You tried to do a full-text search on the field '{field.name}', "
|
||||||
f"but the field is not indexed for full-text search. Use the "
|
f"but the field is not indexed for full-text search. Use the "
|
||||||
f"full_text_search=True option. Docs: TODO"
|
f"full_text_search=True option. Docs: {ERRORS_URL}#E3"
|
||||||
)
|
)
|
||||||
return RediSearchFieldTypes.TEXT
|
return RediSearchFieldTypes.TEXT
|
||||||
|
|
||||||
|
@ -460,7 +467,7 @@ class FindQuery:
|
||||||
elif container_type is not None:
|
elif container_type is not None:
|
||||||
raise QuerySyntaxError(
|
raise QuerySyntaxError(
|
||||||
"Only lists and tuples are supported for multi-value fields. "
|
"Only lists and tuples are supported for multi-value fields. "
|
||||||
"See docs: TODO"
|
f"Docs: {ERRORS_URL}#E4"
|
||||||
)
|
)
|
||||||
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
|
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
|
||||||
# Index numeric Python types as NUMERIC fields, so we can support
|
# Index numeric Python types as NUMERIC fields, so we can support
|
||||||
|
@ -519,8 +526,8 @@ class FindQuery:
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError(
|
raise QueryNotSupportedError(
|
||||||
"Only equals (=), not-equals (!=), and like() "
|
"Only equals (=), not-equals (!=), and like() "
|
||||||
"comparisons are supported for TEXT fields. See "
|
"comparisons are supported for TEXT fields. "
|
||||||
"docs: TODO."
|
f"Docs: {ERRORS_URL}#E5"
|
||||||
)
|
)
|
||||||
elif field_type is RediSearchFieldTypes.NUMERIC:
|
elif field_type is RediSearchFieldTypes.NUMERIC:
|
||||||
if op is Operators.EQ:
|
if op is Operators.EQ:
|
||||||
|
@ -571,7 +578,6 @@ class FindQuery:
|
||||||
value = escaper.escape(value)
|
value = escaper.escape(value)
|
||||||
result += f"-(@{field_name}:{{{value}}})"
|
result += f"-(@{field_name}:{{{value}}})"
|
||||||
elif op is Operators.IN:
|
elif op is Operators.IN:
|
||||||
# TODO: Implement IN, test this...
|
|
||||||
expanded_value = cls.expand_tag_value(value)
|
expanded_value = cls.expand_tag_value(value)
|
||||||
result += f"(@{field_name}:{{{expanded_value}}})"
|
result += f"(@{field_name}:{{{expanded_value}}})"
|
||||||
elif op is Operators.NOT_IN:
|
elif op is Operators.NOT_IN:
|
||||||
|
@ -651,13 +657,12 @@ class FindQuery:
|
||||||
if not field_info or not getattr(field_info, "index", None):
|
if not field_info or not getattr(field_info, "index", None):
|
||||||
raise QueryNotSupportedError(
|
raise QueryNotSupportedError(
|
||||||
f"You tried to query by a field ({field_name}) "
|
f"You tried to query by a field ({field_name}) "
|
||||||
f"that isn't indexed. See docs: TODO"
|
f"that isn't indexed. Docs: {ERRORS_URL}#E6"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError(
|
raise QueryNotSupportedError(
|
||||||
"A query expression should start with either a field "
|
"A query expression should start with either a field "
|
||||||
"or an expression enclosed in parenthesis. See docs: "
|
f"or an expression enclosed in parentheses. Docs: {ERRORS_URL}#E7"
|
||||||
"TODO"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
right = expression.right
|
right = expression.right
|
||||||
|
@ -670,7 +675,7 @@ class FindQuery:
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError(
|
raise QueryNotSupportedError(
|
||||||
"You can only combine two query expressions with"
|
"You can only combine two query expressions with"
|
||||||
"AND (&) or OR (|). See docs: TODO"
|
f"AND (&) or OR (|). Docs: {ERRORS_URL}#E8"
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(right, NegatedExpression):
|
if isinstance(right, NegatedExpression):
|
||||||
|
|
228
docs/errors.md
Normal file
228
docs/errors.md
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
# Errors
|
||||||
|
|
||||||
|
This page lists errors that Redis OM might generate while you're using it, with more context about the error.
|
||||||
|
|
||||||
|
## E1
|
||||||
|
|
||||||
|
> In order to query on a list field, you must define the contents of the list with a type annotation, like: orders: List[Order].
|
||||||
|
|
||||||
|
You will see this error if you try to use an "IN" query, e.g., `await TarotWitch.find(TarotWitch.tarot_cards << "death").all()`, on a field that is not a list.
|
||||||
|
|
||||||
|
In this example, `TarotWitch.tarot_cards` is a list, so the query works:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class TarotWitch(JsonModel):
|
||||||
|
tarot_cards: List[str] = Field(index=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
But if `tarot_cards` was _not_ a list, trying to query with `<<` would have resulted in this error.
|
||||||
|
|
||||||
|
## E2
|
||||||
|
|
||||||
|
> You tried sort by {field_name}, but {self.model} does not define that field as sortable.
|
||||||
|
|
||||||
|
You tried to sort query results by a field that is not sortable. Here is how you mark a field as sortable:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
age: int = Field(index=True, sortable=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** Only an indexed field can be sortable.
|
||||||
|
|
||||||
|
## E3
|
||||||
|
|
||||||
|
>You tried to do a full-text search on the field '{field.name}', but the field is not indexed for full-text search. Use the full_text_search=True option.
|
||||||
|
|
||||||
|
You can make a full-text search with the module (`%`) operator. Such a query looks like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
bio: str = Field(index=True, full_text_search=True, default="")
|
||||||
|
|
||||||
|
Member.find(Member.bio % "beaches").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see this error, it means that the field you are querying (`bio` in the example) is not indexed for full-text search. Make sure you're marking the field both `index=True` and `full_text_search=True`, as in the example.
|
||||||
|
|
||||||
|
## E4
|
||||||
|
|
||||||
|
> Only lists and tuples are supported for multi-value fields.
|
||||||
|
|
||||||
|
This means that you marked a field as `index=True`, but the field is not a type that Redis OM can actually index.
|
||||||
|
|
||||||
|
Specifically, you probably used a _subscripted_ annotation, like `Dict[str, str]`. The only subscripted types that OM can index are `List` and `Tuple`.
|
||||||
|
|
||||||
|
## E5
|
||||||
|
|
||||||
|
> Only equals (=), not-equals (!=), and like() comparisons are supported for TEXT fields.
|
||||||
|
|
||||||
|
You are querying a field you marked as indexed for full-text search. You can only query such fields with the operators for equality (==), non-equality (!=), and like `(%)`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
bio: str = Field(index=True, full_text_search=True, default="")
|
||||||
|
|
||||||
|
# Equality
|
||||||
|
Member.find(Member.bio == "Programmer").all()
|
||||||
|
|
||||||
|
# Non-equality
|
||||||
|
Member.find(Member.bio != "Programmer").all()
|
||||||
|
|
||||||
|
# Like (full-text search). This stems "programming"
|
||||||
|
# to find any matching terms with the same stem,
|
||||||
|
# "program".
|
||||||
|
Member.find(Member.bio % "programming").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
## E6
|
||||||
|
|
||||||
|
> You tried to query by a field ({field_name}) that isn't indexed.
|
||||||
|
|
||||||
|
You wrote a query using a model field that you did not make indexed. You can only query indexed fields. Here is example code that would generate this error:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
first_name: str
|
||||||
|
bio: str = Field(index=True, full_text_search=True, default="")
|
||||||
|
|
||||||
|
# Raises a QueryNotSupportedError because we did not make
|
||||||
|
# `first_name` indexed!
|
||||||
|
Member.find(Member.first_name == "Andrew").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix this by making the field indexed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
first_name: str = Field(index=True)
|
||||||
|
bio: str = Field(index=True, full_text_search=True, default="")
|
||||||
|
|
||||||
|
# Raises a QueryNotSupportedError because we did not make
|
||||||
|
# `first_name` indexed!
|
||||||
|
Member.find(Member.first_name == "Andrew").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
## E7
|
||||||
|
|
||||||
|
> A query expression should start with either a field or an expression enclosed in parentheses.
|
||||||
|
|
||||||
|
We got confused trying to parse your query expression. It's not you, it's us! Some code examples might help...
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
first_name: str = Field(index=True)
|
||||||
|
last_name: str = Field(index=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Queries with a single operator are usually simple:
|
||||||
|
Member.find(Member.first_name == "Andrew").all()
|
||||||
|
|
||||||
|
# If you want to add multiple conditions, you can AND
|
||||||
|
# them together by including the conditions one after
|
||||||
|
# another as arguments.
|
||||||
|
Member.find(Member.first_name=="Andrew",
|
||||||
|
Member.last_name=="Brookins").all()
|
||||||
|
|
||||||
|
# Alternatively, you can separate the conditions with
|
||||||
|
# parenthesis and use an explicit AND.
|
||||||
|
Member.find(
|
||||||
|
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# You can't use `!` to say NOT. Instead, use `~`.
|
||||||
|
Member.find(
|
||||||
|
(Member.first_name == "Andrew") &
|
||||||
|
~(Member.last_name == "Brookins") # <- Notice, this one is NOT now!
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Parenthesis are key to building more complex queries,
|
||||||
|
# like this one.
|
||||||
|
Member.find(
|
||||||
|
~(Member.first_name == "Andrew")
|
||||||
|
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# If you're confused about how Redis OM interprets a query,
|
||||||
|
# use the `tree()` method to visualize the expression tree
|
||||||
|
# for a `FindQuery`.
|
||||||
|
query = Member.find(
|
||||||
|
~(Member.first_name == "Andrew")
|
||||||
|
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||||
|
)
|
||||||
|
print(query.expression.tree)
|
||||||
|
"""
|
||||||
|
┌first_name
|
||||||
|
┌NOT EQ┤
|
||||||
|
| └Andrew
|
||||||
|
AND┤
|
||||||
|
| ┌last_name
|
||||||
|
| ┌EQ┤
|
||||||
|
| | └Brookins
|
||||||
|
└OR┤
|
||||||
|
| ┌last_name
|
||||||
|
└EQ┤
|
||||||
|
└Smith
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## E8
|
||||||
|
|
||||||
|
> You can only combine two query expressions with AND (&) or OR (|).
|
||||||
|
|
||||||
|
The only two operators you can use to combine expressions in a query
|
||||||
|
are `&` and `|`. You may have accidentally used another operator,
|
||||||
|
or Redis OM might be confused. Make sure you are using parentheses
|
||||||
|
to organize your query expressions.
|
||||||
|
|
||||||
|
If you are trying to use "NOT," you can do that by prefixing a query
|
||||||
|
with the `~` operator, like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
first_name: str = Field(index=True)
|
||||||
|
last_name: str = Field(index=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Find people who are not named Andrew.
|
||||||
|
Member.find(~(Member.first_name == "Andrew")).all()
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this form requires parenthesis around the expression
|
||||||
|
that you are "negating." Of course, this example makes more sense
|
||||||
|
with `!=`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from redis_om import JsonModel, Field
|
||||||
|
|
||||||
|
class Member(JsonModel):
|
||||||
|
first_name: str = Field(index=True)
|
||||||
|
last_name: str = Field(index=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Find people who are not named Andrew.
|
||||||
|
Member.find(Member.first_name != "Andrew").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
Still, `~` is useful to negate groups of expressions
|
||||||
|
surrounded by parentheses.
|
|
@ -1,9 +1,11 @@
|
||||||
|
# type: ignore
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Optional, Dict, Set, List
|
from typing import Dict, List, Optional, Set
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -372,24 +374,28 @@ def test_raises_error_with_dataclasses(m):
|
||||||
address_line_1: str
|
address_line_1: str
|
||||||
|
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class InvalidMember(m.BaseHashModel):
|
class InvalidMember(m.BaseHashModel):
|
||||||
address: Address
|
address: Address
|
||||||
|
|
||||||
|
|
||||||
def test_raises_error_with_dicts(m):
|
def test_raises_error_with_dicts(m):
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class InvalidMember(m.BaseHashModel):
|
class InvalidMember(m.BaseHashModel):
|
||||||
address: Dict[str, str]
|
address: Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
def test_raises_error_with_sets(m):
|
def test_raises_error_with_sets(m):
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class InvalidMember(m.BaseHashModel):
|
class InvalidMember(m.BaseHashModel):
|
||||||
friend_ids: Set[str]
|
friend_ids: Set[str]
|
||||||
|
|
||||||
|
|
||||||
def test_raises_error_with_lists(m):
|
def test_raises_error_with_lists(m):
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class InvalidMember(m.BaseHashModel):
|
class InvalidMember(m.BaseHashModel):
|
||||||
friend_ids: List[str]
|
friend_ids: List[str]
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
# type: ignore
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import List, Optional, Set, Dict
|
from typing import Dict, List, Optional, Set
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -687,7 +689,7 @@ async def test_allows_and_serializes_dicts(m):
|
||||||
|
|
||||||
member2 = await ValidMember.get(member.pk)
|
member2 = await ValidMember.get(member.pk)
|
||||||
assert member2 == member
|
assert member2 == member
|
||||||
assert member2.address['address_line_1'] == "hey"
|
assert member2.address["address_line_1"] == "hey"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
Loading…
Reference in a new issue