def _get_access_fields_in_schema( cls, name_foreign_key: str, cls_schema, permission_user: PermissionUser = None, model=None, qs: QueryStringManager = None, ) -> List[str]: """ Получаем список названий полей, которые доступны пользователю и есть в схеме :param name_foreign_key: название "внешнего ключа" :param cls_schema: класс со схемой :param PermissionUser permission_user: пермишены для пользователя :param model: :return: """ # Вытаскиваем модель на которую ссылается "внешний ключ", чтобы получить ограничения на неё # для данного пользователя field_foreign_key = get_model_field(cls_schema, name_foreign_key) mapper = cls._get_model(model, field_foreign_key) current_schema = cls._get_schema(cls_schema, name_foreign_key) permission_for_get: PermissionForGet = permission_user.permission_for_get( mapper) # ограничиваем выгрузку полей в соответствие с пермишенами name_columns = [] if permission_for_get.columns is not None: name_columns = list( set(current_schema._declared_fields.keys()) & permission_for_get.columns) cls._update_qs_fields(current_schema.Meta.type_, name_columns, qs=qs, name_foreign_key=name_foreign_key) return name_columns
def _get_joinedload_object_for_splitted_include( cls, include: str, qs: QueryStringManager, permission_user: PermissionUser, current_schema: Schema, model): """ Processes dot-splitted params from "include" and makes joinedload option for query. """ joinedload_object = None for i, obj in enumerate(include.split(SPLIT_REL)): try: field = get_model_field(current_schema, obj) except Exception as e: raise InvalidInclude(str(e)) if cls._is_access_foreign_key(obj, model, permission_user) is False: continue joinedload_object, current_schema = cls._get_or_update_joinedload_object( joinedload_object=joinedload_object, qs=qs, permission_user=permission_user, model=model, current_schema=current_schema, field=field, include=obj, path_index=i) try: model = cls._get_model(model, field) except ValueError as e: raise InvalidInclude(str(e)) return joinedload_object
def sorting(self): """Return fields to sort by including sort name for SQLAlchemy and row sort parameter for other ORMs :return list: a list of sorting information Example of return value:: [ {'field': 'created_at', 'order': 'desc'}, ] """ if self.qs.get('sort'): sorting_results = [] for sort_field in self.qs['sort'].split(','): field = sort_field.replace('-', '') if SPLIT_REL not in field: if field not in self.schema._declared_fields: raise InvalidSort("{} has no attribute {}".format( self.schema.__name__, field)) if field in get_relationships(self.schema): raise InvalidSort( "You can't sort on {} because it is a relationship field" .format(field)) field = get_model_field(self.schema, field) order = 'desc' if sort_field.startswith('-') else 'asc' sorting_results.append({'field': field, 'order': order}) return sorting_results return []
def eagerload_includes(self, query, qs): """Use eagerload feature of sqlalchemy to optimize data retrieval for include querystring parameter :param Query query: sqlalchemy queryset :param QueryStringManager qs: a querystring manager to retrieve information from url :return Query: the query with includes eagerloaded """ for include in qs.include: joinload_object = None if SPLIT_REL in include: current_schema = self.resource.schema for obj in include.split(SPLIT_REL): try: field = get_model_field(current_schema, obj) except Exception as e: raise InvalidInclude(str(e)) if joinload_object is None: joinload_object = joinedload(field) else: joinload_object = joinload_object.joinedload(field) related_schema_cls = get_related_schema( current_schema, obj) if isinstance(related_schema_cls, SchemaABC): related_schema_cls = related_schema_cls.__class__ else: related_schema_cls = class_registry.get_class( related_schema_cls) current_schema = related_schema_cls else: try: field = get_model_field(self.resource.schema, include) except Exception as e: raise InvalidInclude(str(e)) joinload_object = joinedload(field) query = query.options(joinload_object) return query
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 _get_relationship_data(self): """Get useful data for relationship management""" relationship_field = request.path.split("/")[-1].replace("-", "_") if relationship_field not in get_relationships(self.schema): raise RelationNotFound(f"{self.schema.__name__} has no attribute {relationship_field}") related_type_ = self.schema._declared_fields[relationship_field].type_ related_id_field = self.schema._declared_fields[relationship_field].id_field model_relationship_field = get_model_field(self.schema, relationship_field) return relationship_field, model_relationship_field, related_type_, related_id_field
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 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 __new__(cls, name, bases, d): """Constructor of a resource class""" rv = super().__new__(cls, name, bases, d) if "data_layer" in d: if not isinstance(d["data_layer"], dict): raise Exception( f"You must provide a data layer information as dict in {cls.__name__}" ) if d["data_layer"].get( "class" ) is not None and BaseDataLayer not in inspect.getmro( d["data_layer"]["class"]): raise Exception( f"You must provide a data layer class inherited from BaseDataLayer in {cls.__name__}" ) data_layer_cls = d["data_layer"].get("class", SqlalchemyDataLayer) data_layer_kwargs = d["data_layer"] rv._data_layer = data_layer_cls(data_layer_kwargs) if "schema" in d and "model" in data_layer_kwargs: model = data_layer_kwargs["model"] schema_fields = [ get_model_field(d["schema"], key) for key in d["schema"]._declared_fields.keys() ] invalid_params = validate_model_init_params( model=model, params_names=schema_fields) if invalid_params: raise Exception( f"Construction of {name} failed. Schema '{d['schema'].__name__}' has " f"fields={invalid_params} are not declare in {model.__name__} init parameters" ) rv.decorators = (check_headers, ) if "decorators" in d: rv.decorators += d["decorators"] rv.plugins = d.get("plugins", []) return rv
def _get_joinedload_object_for_include(cls, include, qs, permission_user, current_schema, model): """ Processes params from "include" and makes joinedload option for query """ try: field = get_model_field(current_schema, include) except Exception as e: raise InvalidInclude(str(e)) joinedload_object, _ = cls._get_or_update_joinedload_object( joinedload_object=None, model=model, path_index=0, permission_user=permission_user, qs=qs, field=field, current_schema=current_schema, include=include) return joinedload_object
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