Пример #1
0
 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
Пример #2
0
    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
Пример #3
0
    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 []
Пример #4
0
    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
Пример #5
0
    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))
Пример #6
0
    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
Пример #7
0
    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_
Пример #8
0
    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))
Пример #9
0
    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
Пример #10
0
    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
Пример #11
0
    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_, []
Пример #12
0
    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