Пример #1
0
def bind_index(model,
               name,
               index,
               force=False,
               recursive=True,
               copy=False) -> Index:
    """Bind an index to the model with the given name.

        This method is primarily used during BaseModel.__init_subclass__, although it can be used to easily
        attach a new index to an existing model:

        .. code-block:: python

            import bloop.models

            class User(BaseModel):
                id = Column(String, hash_key=True)
                email = Column(String, dynamo_name="e")


            by_email = GlobalSecondaryIndex(projection="keys", hash_key="email")
            bound = bloop.models.bind_index(User, "by_email", by_email)
            assert bound is by_email

            # rebind with force, and use a copy
            bound = bloop.models.bind_index(User, "by_email", by_email, force=True, copy=True)
            assert bound is not by_email

        If ``name`` or the index's ``dynamo_name`` conflicts with an existing column or index on the model, raises
        :exc:`~bloop.exceptions.InvalidModel` unless ``force`` is True. If ``recursive`` is ``True`` and there are
        existing subclasses of ``model``, a copy of the index will attempt to bind to each subclass.  The recursive
        calls will not force the bind, and will always use a new copy.  If ``copy`` is ``True`` then a copy of the
        provided index is used.  This uses a shallow copy via :meth:`~bloop.models.Index.__copy__`.

        :param model:
            The model to bind the index to.
        :param name:
            The name to bind the index as.  In effect, used for ``setattr(model, name, index)``
        :param index:
            The index to bind to the model.
        :param force:
            Unbind existing columns or indexes with the same name or dynamo_name.  Default is False.
        :param recursive:
            Bind to each subclass of this model.  Default is False.
        :param copy:
            Use a copy of the index instead of the index directly.  Default is False.
        :return:
            The bound index.  This is a new column when ``copy`` is True, otherwise the input index.
        """
    if not subclassof(model, BaseModel):
        raise InvalidModel(f"{model} is not a subclass of BaseModel")
    meta = model.Meta
    if copy:
        index = copyfn(index)
    # TODO elif index.model is not None: logger.warning(f"Trying to rebind index bound to {index.model}")
    index._name = name
    safe_repr = unbound_repr(index)

    # Guard against name, dynamo_name collisions; if force=True, unbind any matches
    same_dynamo_name = (
        util.index(meta.columns, "dynamo_name").get(index.dynamo_name)
        or util.index(meta.indexes, "dynamo_name").get(index.dynamo_name))
    same_name = (meta.columns_by_name.get(index.name)
                 or util.index(meta.indexes, "name").get(index.name))

    if isinstance(index, LocalSecondaryIndex) and not meta.range_key:
        raise InvalidModel("An LSI requires the Model to have a range key.")

    if force:
        if same_name:
            unbind(meta, name=index.name)
        if same_dynamo_name:
            unbind(meta, dynamo_name=index.dynamo_name)
    else:
        if same_name:
            raise InvalidModel(
                f"The index {safe_repr} has the same name as an existing index "
                f"or column {same_name}.  Did you mean to bind with force=True?"
            )
        if same_dynamo_name:
            raise InvalidModel(
                f"The index {safe_repr} has the same dynamo_name as an existing "
                f"index or column {same_name}.  Did you mean to bind with force=True?"
            )

    # success!
    # --------------------------------
    index.model = meta.model
    meta.indexes.add(index)
    setattr(meta.model, name, index)

    if isinstance(index, LocalSecondaryIndex):
        meta.lsis.add(index)
    if isinstance(index, GlobalSecondaryIndex):
        meta.gsis.add(index)

    try:
        refresh_index(meta, index)
    except KeyError as e:
        raise InvalidModel(
            "Index expected a hash or range key that does not exist") from e

    if recursive:
        for subclass in util.walk_subclasses(meta.model):
            try:
                bind_index(subclass,
                           name,
                           index,
                           force=False,
                           recursive=False,
                           copy=True)
            except InvalidModel:
                pass

    return index
Пример #2
0
def bind_column(model,
                name,
                column,
                force=False,
                recursive=False,
                copy=False) -> Column:
    """Bind a column to the model with the given name.

    This method is primarily used during BaseModel.__init_subclass__, although it can be used to easily
    attach a new column to an existing model:

    .. code-block:: python

        import bloop.models

        class User(BaseModel):
            id = Column(String, hash_key=True)


        email = Column(String, dynamo_name="e")
        bound = bloop.models.bind_column(User, "email", email)
        assert bound is email

        # rebind with force, and use a copy
        bound = bloop.models.bind_column(User, "email", email, force=True, copy=True)
        assert bound is not email

    If an existing index refers to this column, it will be updated to point to the new column
    using :meth:`~bloop.models.refresh_index`, including recalculating the index projection.
    Meta attributes including ``Meta.columns``, ``Meta.hash_key``, etc. will be updated if necessary.

    If ``name`` or the column's ``dynamo_name`` conflicts with an existing column or index on the model, raises
    :exc:`~bloop.exceptions.InvalidModel` unless ``force`` is True. If ``recursive`` is ``True`` and there are
    existing subclasses of ``model``, a copy of the column will attempt to bind to each subclass.  The recursive
    calls will not force the bind, and will always use a new copy.  If ``copy`` is ``True`` then a copy of the
    provided column is used.  This uses a shallow copy via :meth:`~bloop.models.Column.__copy__`.

    :param model:
        The model to bind the column to.
    :param name:
        The name to bind the column as.  In effect, used for ``setattr(model, name, column)``
    :param column:
        The column to bind to the model.
    :param force:
        Unbind existing columns or indexes with the same name or dynamo_name.  Default is False.
    :param recursive:
        Bind to each subclass of this model.  Default is False.
    :param copy:
        Use a copy of the column instead of the column directly.  Default is False.
    :return:
        The bound column.  This is a new column when ``copy`` is True, otherwise the input column.
    """
    if not subclassof(model, BaseModel):
        raise InvalidModel(f"{model} is not a subclass of BaseModel")
    meta = model.Meta
    if copy:
        column = copyfn(column)
    # TODO elif column.model is not None: logger.warning(f"Trying to rebind column bound to {column.model}")
    column._name = name
    safe_repr = unbound_repr(column)

    # Guard against name, dynamo_name collisions; if force=True, unbind any matches
    same_dynamo_name = (
        util.index(meta.columns, "dynamo_name").get(column.dynamo_name)
        or util.index(meta.indexes, "dynamo_name").get(column.dynamo_name))
    same_name = (meta.columns_by_name.get(column.name)
                 or util.index(meta.indexes, "name").get(column.name))

    if column.hash_key and column.range_key:
        raise InvalidModel(
            f"Tried to bind {safe_repr} as both a hash and range key.")

    if force:
        if same_name:
            unbind(meta, name=column.name)
        if same_dynamo_name:
            unbind(meta, dynamo_name=column.dynamo_name)
    else:
        if same_name:
            raise InvalidModel(
                f"The column {safe_repr} has the same name as an existing column "
                f"or index {same_name}.  Did you mean to bind with force=True?"
            )
        if same_dynamo_name:
            raise InvalidModel(
                f"The column {safe_repr} has the same dynamo_name as an existing "
                f"column or index {same_name}.  Did you mean to bind with force=True?"
            )
        if column.hash_key and meta.hash_key:
            raise InvalidModel(
                f"Tried to bind {safe_repr} but {meta.model} "
                f"already has a different hash_key: {meta.hash_key}")
        if column.range_key and meta.range_key:
            raise InvalidModel(
                f"Tried to bind {safe_repr} but {meta.model} "
                f"already has a different range_key: {meta.range_key}")

    # success!
    # --------------------------------
    column.model = meta.model
    meta.columns.add(column)
    meta.columns_by_name[name] = column
    setattr(meta.model, name, column)

    if column.hash_key:
        meta.hash_key = column
        meta.keys.add(column)
    if column.range_key:
        meta.range_key = column
        meta.keys.add(column)

    try:
        for index in meta.indexes:
            refresh_index(meta, index)
    except KeyError as e:
        raise InvalidModel(
            f"Binding column {column} removed a required column for index {unbound_repr(index)}"
        ) from e

    if recursive:
        for subclass in util.walk_subclasses(meta.model):
            try:
                bind_column(subclass,
                            name,
                            column,
                            force=False,
                            recursive=False,
                            copy=True)
            except InvalidModel:
                pass

    return column