11"""Helper to create sqlalchemy filters according to filter querystring parameter"""
2- from typing import Any , List , Tuple , Type , Union
3-
4- from pydantic import BaseModel
2+ import logging
3+ from typing import (
4+ Any ,
5+ Callable ,
6+ Dict ,
7+ List ,
8+ Optional ,
9+ Tuple ,
10+ Type ,
11+ Union ,
12+ )
13+
14+ from pydantic import BaseConfig , BaseModel
515from pydantic .fields import ModelField
16+ from pydantic .validators import _VALIDATORS , find_validators
617from sqlalchemy import and_ , not_ , or_
718from sqlalchemy .orm import InstrumentedAttribute , aliased
819from sqlalchemy .sql .elements import BinaryExpression
920
1021from fastapi_jsonapi .data_layers .shared import create_filters_or_sorts
1122from fastapi_jsonapi .data_typing import TypeModel , TypeSchema
1223from fastapi_jsonapi .exceptions import InvalidFilters , InvalidType
24+ from fastapi_jsonapi .exceptions .json_api import HTTPException
1325from fastapi_jsonapi .schema import get_model_field , get_relationships
1426from fastapi_jsonapi .splitter import SPLIT_REL
1527from fastapi_jsonapi .utils .sqla import get_related_model_cls
1628
29+ log = logging .getLogger (__name__ )
30+
1731Filter = BinaryExpression
1832Join = List [Any ]
1933
2236 List [Join ],
2337]
2438
39+ # The mapping with validators using by to cast raw value to instance of target type
40+ REGISTERED_PYDANTIC_TYPES : Dict [Type , List [Callable ]] = dict (_VALIDATORS )
41+
42+ cast_failed = object ()
43+
2544
2645def create_filters (model : Type [TypeModel ], filter_info : Union [list , dict ], schema : Type [TypeSchema ]):
2746 """
@@ -48,6 +67,21 @@ def __init__(self, model: Type[TypeModel], filter_: dict, schema: Type[TypeSchem
4867 self .filter_ = filter_
4968 self .schema = schema
5069
70+ def _cast_value_with_scheme (self , field_types : List [ModelField ], value : Any ) -> Tuple [Any , List [str ]]:
71+ errors : List [str ] = []
72+ casted_value = cast_failed
73+
74+ for field_type in field_types :
75+ try :
76+ if isinstance (value , list ): # noqa: SIM108
77+ casted_value = [field_type (item ) for item in value ]
78+ else :
79+ casted_value = field_type (value )
80+ except (TypeError , ValueError ) as ex :
81+ errors .append (str (ex ))
82+
83+ return casted_value , errors
84+
5185 def create_filter (self , schema_field : ModelField , model_column , operator , value ):
5286 """
5387 Create sqlalchemy filter
@@ -78,19 +112,94 @@ def create_filter(self, schema_field: ModelField, model_column, operator, value)
78112 types = [i .type_ for i in fields ]
79113 clear_value = None
80114 errors : List [str ] = []
81- for i_type in types :
82- try :
83- if isinstance (value , list ): # noqa: SIM108
84- clear_value = [i_type (item ) for item in value ]
85- else :
86- clear_value = i_type (value )
87- except (TypeError , ValueError ) as ex :
88- errors .append (str (ex ))
115+
116+ pydantic_types , userspace_types = self ._separate_types (types )
117+
118+ if pydantic_types :
119+ if isinstance (value , list ):
120+ clear_value , errors = self ._cast_iterable_with_pydantic (pydantic_types , value )
121+ else :
122+ clear_value , errors = self ._cast_value_with_pydantic (pydantic_types , value )
123+
124+ if clear_value is None and userspace_types :
125+ log .warning ("Filtering by user type values is not properly tested yet. Use this on your own risk." )
126+
127+ clear_value , errors = self ._cast_value_with_scheme (types , value )
128+
129+ if clear_value is cast_failed :
130+ raise InvalidType (
131+ detail = f"Can't cast filter value `{ value } ` to arbitrary type." ,
132+ errors = [HTTPException (status_code = InvalidType .status_code , detail = str (err )) for err in errors ],
133+ )
134+
89135 # Если None, при этом поле обязательное (среди типов в аннотации нет None, то кидаем ошибку)
90136 if clear_value is None and not any (not i_f .required for i_f in fields ):
91137 raise InvalidType (detail = ", " .join (errors ))
92138 return getattr (model_column , self .operator )(clear_value )
93139
140+ def _separate_types (self , types : List [Type ]) -> Tuple [List [Type ], List [Type ]]:
141+ """
142+ Separates the types into two kinds. The first are those for which
143+ there are already validators defined by pydantic - str, int, datetime
144+ and some other built-in types. The second are all other types for which
145+ the `arbitrary_types_allowed` config is applied when defining the pydantic model
146+ """
147+ pydantic_types = [
148+ # skip format
149+ type_
150+ for type_ in types
151+ if type_ in REGISTERED_PYDANTIC_TYPES
152+ ]
153+ userspace_types = [
154+ # skip format
155+ type_
156+ for type_ in types
157+ if type_ not in REGISTERED_PYDANTIC_TYPES
158+ ]
159+ return pydantic_types , userspace_types
160+
161+ def _cast_value_with_pydantic (
162+ self ,
163+ types : List [Type ],
164+ value : Any ,
165+ ) -> Tuple [Optional [Any ], List [str ]]:
166+ result_value , errors = None , []
167+
168+ for type_to_cast in types :
169+ for validator in find_validators (type_to_cast , BaseConfig ):
170+ try :
171+ result_value = validator (value )
172+ return result_value , errors
173+ except Exception as ex :
174+ errors .append (str (ex ))
175+
176+ return None , errors
177+
178+ def _cast_iterable_with_pydantic (self , types : List [Type ], values : List ) -> Tuple [List , List [str ]]:
179+ type_cast_failed = False
180+ failed_values = []
181+
182+ result_values : List [Any ] = []
183+ errors : List [str ] = []
184+
185+ for value in values :
186+ casted_value , cast_errors = self ._cast_value_with_pydantic (types , value )
187+ errors .extend (cast_errors )
188+
189+ if casted_value is None :
190+ type_cast_failed = True
191+ failed_values .append (value )
192+
193+ continue
194+
195+ result_values .append (casted_value )
196+
197+ if type_cast_failed :
198+ msg = f"Can't parse items { failed_values } of value { values } "
199+ raise InvalidFilters (msg )
200+
201+ return result_values , errors
202+
94203 def resolve (self ) -> FilterAndJoins : # noqa: PLR0911
95204 """Create filter for a particular node of the filter tree"""
96205 if "or" in self .filter_ :
0 commit comments