def op(self): """Return the operator of the node :return str: the operator to use in the filter """ try: return self.filter_['op'] except KeyError: raise InvalidFilters("Can't find op of a filter")
def value(self): """Get the value to filter on :return: the value to filter on """ if self.filter_.get('field') is not None: try: result = getattr(self.model, self.filter_['field']) except AttributeError: raise InvalidFilters("{} has no attribute {}".format( self.model.__name__, self.filter_['field'])) else: return result else: if 'val' not in self.filter_: raise InvalidFilters("Can't find value or field in a filter") return self.filter_['val']
def name(self): """Return the name of the node or raise a BadRequest exception :return str: the name of the field to filter on """ name = self.filter_.get('name') if name is None: raise InvalidFilters("Can't find name of a filter") if SPLIT_REL in name: name = name.split(SPLIT_REL)[0] if name not in self.schema._declared_fields: raise InvalidFilters("{} has no attribute {}".format( self.schema.__name__, name)) return name
def column(self): """Get the column object """ field = self.name model_field = get_model_field(self.schema, field) try: return getattr(self.model, model_field) except AttributeError: raise InvalidFilters("{} has no attribute {}".format( self.model.__name__, model_field))
def related_schema(self): """Get the related schema of a relationship field :return Schema: the related schema """ relationship_field = self.name if relationship_field not in get_relationships(self.schema): raise InvalidFilters("{} has no relationship attribute {}".format( self.schema.__name__, relationship_field)) return self.schema._declared_fields[ relationship_field].schema.__class__
def operator(self): """Get the function operator from his name :return callable: a callable to make operation on a column """ operators = (self.op, self.op + '_', '__' + self.op + '__') for op in operators: if hasattr(self.column, op): return op raise InvalidFilters("{} has no operator {}".format( self.column.key, self.op))
def related_model(self): """Get the related model of a relationship field :return DeclarativeMeta: the related model """ relationship_field = self.name if relationship_field not in get_relationships(self.schema): raise InvalidFilters("{} has no relationship attribute {}".format( self.schema.__name__, relationship_field)) return getattr(self.model, get_model_field( self.schema, relationship_field)).property.mapper.class_
def deserialize_field(marshmallow_field: fields.Field, value: Any) -> Any: """ Deserialize filter/sort value :param marshmallow_field: marshmallow field type :param value: filter/sort value :return: """ try: if isinstance(value, list) and type(marshmallow_field) in STANDARD_MARSHMALLOW_FIELDS: return [marshmallow_field.deserialize(i_value) for i_value in value] if not isinstance(value, list) and isinstance(marshmallow_field, fields.List): return marshmallow_field.deserialize([value]) return marshmallow_field.deserialize(value) except ValidationError: raise InvalidFilters(f'Bad filter value: {value!r}')
def filters(self): """Return filters from query string. :return list: filter information """ results = [] filters = self.qs.get('filter') if filters is not None: try: results.extend(json.loads(filters)) except (ValueError, TypeError): raise InvalidFilters("Parse error") if self._get_key_values('filter['): results.extend(self._simple_filters(self._get_key_values('filter['))) return results
def column(self): """Get the column object :param DeclarativeMeta model: the model :param str field: the field :return InstrumentedAttribute: the column to filter on """ field = self.name model_field = get_model_field(self.schema, field) try: return getattr(self.model, model_field) except AttributeError: raise InvalidFilters("{} has no attribute {}".format( self.model.__name__, model_field))
def _isinstance_jsonb(cls, schema: Schema, filter_name: str) -> bool: """ Определяем относится ли фильтр к relationship или к полю JSONB :param schema: :param filter_name: :return: """ fields = filter_name.split(SPLIT_REL) for i, i_field in enumerate(fields): if isinstance( getattr(schema._declared_fields[i_field], "schema", None), SchemaJSONB): if i == (len(fields) - 1): raise InvalidFilters( f"Invalid JSONB filter: {filter_name}") return True elif isinstance(schema._declared_fields[i_field], Relationship): schema = schema._declared_fields[i_field].schema else: return False
def _create_filter(self, self_nested: Any, marshmallow_field, model_column, operator, value): """ Create sqlalchemy filter :param Nested self_nested: :param marshmallow_field: :param model_column: column sqlalchemy :param operator: :param value: :return: """ fields = self_nested.filter_["name"].split(SPLIT_REL) field_in_jsonb = fields[-1] schema = getattr(marshmallow_field, "schema", None) if isinstance(marshmallow_field, Relationship): # If filtering by JSONB field of another model is in progress mapper = model_column.mapper.class_ sqlalchemy_relationship_name = get_model_field(schema, fields[1]) self_nested.filter_["name"] = SPLIT_REL.join(fields[1:]) marshmallow_field = marshmallow_field.schema._declared_fields[ fields[1]] join_list = [[model_column]] model_column = getattr(mapper, sqlalchemy_relationship_name) filter, joins = self._create_filter(self_nested, marshmallow_field, model_column, operator, value) join_list += joins return filter, join_list elif not isinstance(schema, SchemaJSONB): raise InvalidFilters( f"Invalid JSONB filter: {SPLIT_REL.join(field_in_jsonb)}") self_nested.filter_["name"] = SPLIT_REL.join(fields[:-1]) try: for field in fields[1:]: marshmallow_field = marshmallow_field.schema._declared_fields[ field] except KeyError as e: raise InvalidFilters( f'There is no "{e}" attribute in the "{fields[0]}" field.') if hasattr(marshmallow_field, f"_{operator}_sql_filter_"): """ У marshmallow field может быть реализована своя логика создания фильтра для sqlalchemy для определённого оператора. Чтобы реализовать свою логику создания фильтра для определённого оператора необходимо реализовать в классе поля методы (название метода строится по следующему принципу `_<тип оператора>_sql_filter_`). Также такой метод должен принимать ряд параметров * marshmallow_field - объект класса поля marshmallow * model_column - объект класса поля sqlalchemy * value - значения для фильтра * operator - сам оператор, например: "eq", "in"... """ for field in fields[1:-1]: model_column = model_column.op("->")(field) model_column = model_column.op("->>")(field_in_jsonb) return ( getattr(marshmallow_field, f"_{operator}_sql_filter_")( marshmallow_field=marshmallow_field, model_column=model_column, value=value, operator=self_nested.operator, ), [], ) # Нужно проводить валидацию и делать десериализацию значение указанных в фильтре, так как поля Enum # например выгружаются как 'name_value(str)', а в БД хранится как просто число value = deserialize_field(marshmallow_field, value) property_type = self.get_property_type( marshmallow_field=marshmallow_field, schema=self_nested.schema) for field in fields[1:-1]: model_column = model_column.op("->")(field) extra_field = model_column.op("->>")(field_in_jsonb) filter_ = "" if property_type in {bool, int, str, bytes, Decimal}: field = cast(extra_field, self.mapping_type_to_sql_type[property_type]) if value is None: filter_ = field.is_(None) else: filter_ = getattr(field, self_nested.operator)(value) elif property_type == list: filter_ = model_column.op("->")(field_in_jsonb).op("?")( value[0] if is_seq_collection(value) else value) if operator in ["notin", "notin_"]: filter_ = not_(filter_) return filter_, []
def _create_sort(self, self_nested: Any, marshmallow_field, model_column, order): """ Create sqlalchemy sort :param Nested self_nested: :param marshmallow_field: :param model_column: column sqlalchemy :param str order: asc | desc :return: """ fields = self_nested.sort_["field"].split(SPLIT_REL) schema = getattr(marshmallow_field, "schema", None) if isinstance(marshmallow_field, Relationship): # If sorting by JSONB field of another model is in progress mapper = model_column.mapper.class_ sqlalchemy_relationship_name = get_model_field(schema, fields[1]) self_nested.sort_["field"] = SPLIT_REL.join(fields[1:]) marshmallow_field = marshmallow_field.schema._declared_fields[ fields[1]] model_column = getattr(mapper, sqlalchemy_relationship_name) return self._create_sort(self_nested, marshmallow_field, model_column, order) elif not isinstance(schema, SchemaJSONB): raise InvalidFilters( f"Invalid JSONB sort: {SPLIT_REL.join(self_nested.fields)}") self_nested.sort_["field"] = SPLIT_REL.join(fields[:-1]) field_in_jsonb = fields[-1] try: for field in fields[1:]: marshmallow_field = marshmallow_field.schema._declared_fields[ field] except KeyError as e: raise InvalidFilters( f'There is no "{e}" attribute in the "{fields[0]}" field.') if hasattr(marshmallow_field, f"_{order}_sql_sort_"): """ У marshmallow field может быть реализована своя логика создания сортировки для sqlalchemy для определённого типа ('asc', 'desc'). Чтобы реализовать свою логику создания сортировка для определённого оператора необходимо реализовать в классе поля методы (название метода строится по следующему принципу `_<тип сортировки>_sql_filter_`). Также такой метод должен принимать ряд параметров * marshmallow_field - объект класса поля marshmallow * model_column - объект класса поля sqlalchemy """ # All values between the first and last field will be the path to the desired value by which to sort, # so we write the path through "->" for field in fields[1:-1]: model_column = model_column.op("->")(field) model_column = model_column.op("->>")(field_in_jsonb) return getattr(marshmallow_field, f"_{order}_sql_sort_")( marshmallow_field=marshmallow_field, model_column=model_column) property_type = self.get_property_type( marshmallow_field=marshmallow_field, schema=self_nested.schema) for field in fields[1:-1]: model_column = model_column.op("->")(field) extra_field = model_column.op("->>")(field_in_jsonb) sort = "" order_op = desc_op if order == "desc" else asc_op if property_type in self.mapping_type_to_sql_type: if sqlalchemy.__version__ >= "1.1": sort = order_op( extra_field.astext.cast( self.mapping_type_to_sql_type[property_type])) else: sort = order_op( extra_field.cast( self.mapping_type_to_sql_type[property_type])) return sort