bloop/types.py
class String(Type):
    python_type = str
    backing_type = STRING

    def dynamo_load(self, value, *, context, **kwargs):
        if not value:
            return ""
        return value

    def dynamo_dump(self, value, *, context, **kwargs):
        if not value:
            return None
        return value
tests/helpers/models.py
class Document(BaseModel):
    id = Column(Integer, hash_key=True)
    data = Column(DocumentType)
    numbers = Column(List(Integer))
    value = Column(Number)
    another_value = Column(Number)
    some_string = Column(String)
    nested_numbers = Column(List(List(Integer)))
bloop/types.py
class List(Type):
    """Holds values of a single type.

    Similar to :class:`~bloop.types.Set` because it requires a single type.  However, that type
    can be another List, or :class:`~bloop.types.Map`, or :class:`~bloop.types.Boolean`.  This is restricted
    to a single type even though DynamoDB is not because there is no way to know which Type to load a DynamoDB value
    with.

    For example, ``{"S": "6d8b54a2-fa07-47e1-9305-717699459293"}`` could be loaded with
    :class:`~bloop.types.UUID`, :class:`~bloop.types.String`, or any other class that is backed by "S".

    .. code-block:: python

        SingleQuizAnswers = List(String)

        class AnswerBook(BaseModel):
            ...
            all_answers = Column(List(SingleQuizAnswers))

    .. seealso::

        To store arbitrary lists, see :class:`~bloop.types.DynamicList`.

    :param typedef: The type to use when loading and saving values in this list.
    """
    python_type = collections.abc.Iterable
    backing_type = LIST

    def __init__(self, typedef):
        self.inner_typedef = type_instance(typedef)
        super().__init__()

    def __getitem__(self, key):
        return self.inner_typedef

    def dynamo_load(self, values, *, context, **kwargs):
        if values is None:
            return list()
        # noinspection PyProtectedMember
        load = self.inner_typedef._load
        return [
            load(value, context=context, **kwargs)
            for value in values]

    def dynamo_dump(self, values, *, context, **kwargs):
        if values is None:
            return None
        # noinspection PyProtectedMember
        dump = guard_no_action(self.inner_typedef._dump)
        dumped = (dump(value, context=context, **kwargs) for value in values)
        return [value for value in dumped if value is not None] or None
bloop/types.py
class Binary(Type):
    python_type = bytes
    backing_type = BINARY

    def dynamo_load(self, value, *, context, **kwargs):
        if value is None:
            return b""
        return base64.b64decode(value)

    def dynamo_dump(self, value, *, context, **kwargs):
        if not value:
            return None
        return base64.b64encode(value).decode("utf-8")
bloop/types.py
class Set(Type):
    """Generic set type.  Must provide an inner type.

    .. code-block:: python

        class Customer(BaseModel):
            id = Column(Integer, hash_key=True)
            account_ids = Column(Set(UUID))

    :param typedef: The type to use when loading and saving values in this set.
        Must have a ``backing_type`` of "S", "N", or "B".
    """
    python_type = collections.abc.Set

    def __init__(self, typedef):
        self.inner_typedef = type_instance(typedef)
        self.backing_type = typedef.backing_type + "S"
        if self.backing_type not in {"NS", "SS", "BS"}:
            raise TypeError("{!r} is not a valid set type.".format(self.backing_type))
        super().__init__()

    def dynamo_load(self, values, *, context, **kwargs):
        if values is None:
            return set()
        return set(
            self.inner_typedef.dynamo_load(value, context=context, **kwargs)
            for value in values)

    def dynamo_dump(self, values, *, context, **kwargs):
        if values is None:
            return None
        dumped = []
        dump = guard_no_action(self.inner_typedef.dynamo_dump)
        for value in values:
            value = dump(value, context=context, **kwargs)
            if value is not None:
                dumped.append(value)
        return dumped or None
bloop/types.py
class Boolean(Type):
    python_type = bool
    backing_type = BOOLEAN

    def dynamo_load(self, value, *, context, **kwargs):
        if value is None:
            return None
        return bool(value)

    def dynamo_dump(self, value, *, context, **kwargs):
        if value is None:
            return None
        return bool(value)
bloop/types.py
class Integer(Number):
    """Truncates values when loading or dumping.

    For example, ``3.14`` in DynamoDB is loaded as ``3``. If a value is ``7.5``
    locally, it's stored in DynamoDB as ``7``.
    """
    python_type = int

    def dynamo_load(self, value, *, context, **kwargs):
        if value is None:
            return None
        number = super().dynamo_load(value, context=context, **kwargs)
        return int(number)

    def dynamo_dump(self, value, *, context, **kwargs):
        if value is None:
            return None
        value = int(value)
        return super().dynamo_dump(value, context=context, **kwargs)
bloop/models.py
class BaseModel:
    """Abstract base that all models derive from.

    Provides a basic ``__init__`` method that takes ``**kwargs`` whose
    keys are columns names:

    .. code-block:: python

        class URL(BaseModel):
            id = Column(UUID, hash_key=True)
            ip = Column(IPv6)
            name = Column(String)

        url = URL(id=uuid.uuid4(), name="google")

    By default, the ``__init__`` method is not called when new instances are
    required, for example when iterating results from Query, Scan or a Stream.

    """
    class Meta(IMeta):
        abstract = True

    def __init__(self, **attrs):
        # Only set values from **attrs if there's a
        # corresponding `name` for a column in the model
        for column in self.Meta.columns:
            value = attrs.get(column.name, missing)
            if value is missing:
                value = column.default()
            if value is not missing:
                setattr(self, column.name, value)

    def __init_subclass__(cls: type, **kwargs):
        ensure_hash(cls)
        meta = initialize_meta(cls)

        # before we start binding, we should ensure that no combination of parent classes
        # will cause conflicts.  For example:
        #   class C(A, B) where
        #       A has a column named "foo" and dynamo_name "ddb"
        #       B has a column named "bar" and dynamo_name "ddb"
        # both A and B are valid mixins, but C must fail because there isn't a 1:1 binding to the "ddb" column.
        #
        # TODO | for now, we'll assume that the class being defined is special, and can replace columns with the
        # TODO | same dynamo_name.  In the example above, that would mean C has a column named "baz" and dynamo_name
        # TODO | "ddb" which would prevent the parent columns "foo" and "bar" from binding to the child class.

        modeled_attrs = set((attr for (_, attr) in inspect.getmembers(cls, lambda x: isinstance(x, (Column, Index)))))
        local_attrs = {
            name: value
            for name, value in cls.__dict__.items()
            if isinstance(value, (Column, Index))
        }
        derived_attrs = modeled_attrs - set(local_attrs.values())

        # 0.0 Pre-validation for collisions in derived columns/indexes
        dynamo_names = [x.dynamo_name for x in derived_attrs]
        collisions = [name for name, count in collections.Counter(dynamo_names).items() if count > 1]
        if collisions:
            collisions.sort()
            raise InvalidModel(
                f"The model {cls.__name__} subclasses one or more models with conflicting "
                f"column or index definitions for the following values of dynamo_name: {collisions}")
        derived_hash_keys = set((x.name for x in derived_attrs if isinstance(x, Column) and x.hash_key))
        if len(derived_hash_keys) > 1:
            derived_hash_keys = sorted(derived_hash_keys)
            raise InvalidModel(
                f"The model {cls.__name__} subclasses one or more models that declare multiple "
                f"columns as the hash key: {derived_hash_keys}")
        derived_range_keys = set((x.name for x in derived_attrs if isinstance(x, Column) and x.range_key))
        if len(derived_range_keys) > 1:
            derived_range_keys = sorted(derived_range_keys)
            raise InvalidModel(
                f"The model {cls.__name__} subclasses one or more models that declare multiple "
                f"columns as the range key: {derived_range_keys}")

        # 0.1 Pre-validation for collisions in local columns/indexes
        dynamo_names = [x.dynamo_name for x in local_attrs.values()]
        collisions = [name for name, count in collections.Counter(dynamo_names).items() if count > 1]
        if collisions:
            collisions.sort()
            raise InvalidModel(
                f"The model {cls.__name__} contains conflicting column or index definitions for the "
                f"following values of dynamo_name: {collisions}")
        local_hash_keys = [x.name for x in local_attrs.values() if isinstance(x, Column) and x.hash_key]
        if len(local_hash_keys) > 1:
            local_hash_keys = sorted(local_hash_keys)
            raise InvalidModel(
                f"The model {cls.__name__} defines multiple columns as hash columns: {local_hash_keys}")
        local_range_keys = [x.name for x in local_attrs.values() if isinstance(x, Column) and x.range_key]
        if len(local_range_keys) > 1:
            local_range_keys = sorted(local_range_keys)
            raise InvalidModel(
                f"The model {cls.__name__} defines multiple columns as range columns: {local_range_keys}")

        # 1.0 Bind derived columns so they can be referenced by derived indexes
        for attr in derived_attrs:
            if isinstance(attr, Column):
                bind_column(cls, attr.name, attr, copy=True)

        # 1.1 Bind derived indexes
        for attr in derived_attrs:
            if isinstance(attr, Index):
                bind_index(cls, attr.name, attr, copy=True)

        # 1.2 Bind local columns, allowing them to overwrite existing columns
        for name, attr in local_attrs.items():
            if isinstance(attr, Column):
                bind_column(cls, name, attr, force=True)

        # 1.3 Bind local indexes, allowing them to overwrite existing indexes
        for name, attr in local_attrs.items():
            if isinstance(attr, Index):
                bind_index(cls, name, attr, force=True)

        # 2.0 Ensure concrete models are valid
        # Currently, this just checks that a hash key is defined
        if not meta.abstract and not meta.hash_key:
            raise InvalidModel(f"{meta.model.__name__!r} has no hash key.")

        validate_stream(meta)
        validate_ttl(meta)
        validate_encryption(meta)
        validate_backups(meta)
        validate_billing(meta)

        # 3.0 Fire model_created for customizing the class after creation
        model_created.send(None, model=cls)

    def __repr__(self):
        attrs = ", ".join("{}={!r}".format(*item) for item in loaded_columns(self))
        return f"{self.__class__.__name__}({attrs})"
tests/helpers/models.py
class User(BaseModel):
    id = Column(String, hash_key=True)
    age = Column(Integer)
    name = Column(String)
    email = Column(String)
    joined = Column(DateTime, dynamo_name="j")
    by_email = GlobalSecondaryIndex(hash_key="email", projection="all")
bloop/actions.py
class ActionType(enum.Enum):
class Action:
    def __init__(self, wire_key: str, fmt: str, nestable: bool):
    def render(self, name_ref, value_ref):
    def new_action(self, value) -> "Action":
    def __new__(cls, action_type: ActionType, value):
    def __init__(self, action_type: ActionType, value):
    def type(self):
    def value(self):
    def __repr__(self):
    def __eq__(self, other):
def unwrap(x: Union[Action, Any]) -> Any:
def wrap(x: Any) -> Action:
def add(value):
def delete(value):
def remove(value=None):
def set(value):
NONE_SENTINEL = Action(ActionType.Remove, None)
bloop/types.py
class Map(Type):
    """Mapping of fixed keys and their Types.

    .. code-block:: python

        Metadata = Map(**{
            "created": DateTime,
            "referrer": UUID,
            "cache": String
        })

        Product = Map(
            id=Integer,
            metadata=Metadata,
            price=Number
        )

        class ProductCatalog(BaseModel):
            ...
            all_products = Column(List(Product))

    .. seealso::

        To store arbitrary documents, see :class:`~bloop.types.DynamicMap`.

    :param types: *(Optional)* specifies the keys and their Types when loading and dumping the Map.
        Any keys that aren't specified in ``types`` are ignored when loading and dumping.
    """
    python_type = collections.abc.Mapping
    backing_type = MAP

    def __init__(self, **types):
        self.types = {k: type_instance(t) for k, t in types.items()}
        super().__init__()

    def __getitem__(self, key):
        """Overload allows easy nested access to types"""
        return self.types[key]

    def dynamo_load(self, values, *, context, **kwargs):
        if values is None:
            values = dict()
        loaded = {}
        for key, typedef in self.types.items():
            # noinspection PyProtectedMember
            value = typedef._load(values.get(key, None), context=context, **kwargs)
            loaded[key] = value
        return loaded

    def dynamo_dump(self, values, *, context, **kwargs):
        if values is None:
            return None
        dumped = {}
        for key, typedef in self.types.items():
            # noinspection PyProtectedMember
            dump = guard_no_action(typedef._dump)
            value = dump(values.get(key, None), context=context, **kwargs)
            if value is not None:
                dumped[key] = value
        return dumped or None
bloop/models.py
class Column(ComparisonMixin):
    model: BaseModel
    """Represents a single attribute in DynamoDB.

    :param typedef: The type of this attribute.  Can be either a :class:`~bloop.types.Type` or
        an instance thereof.  If a type class is provided, the column will call the constructor without arguments
        to create an instance.  For example, ``Column(Integer)`` and ``Column(Integer())`` are equivalent.
    :param bool hash_key: *(Optional)* True if this is the model's hash key.
        A model must have exactly one Column with ``hash_key=True``.  Default is False.
    :param bool range_key:  *(Optional)* True if this is the model's range key.
        A model can have at most one Column with
        ``range_key=True``.  Default is False.
    :param str dynamo_name: *(Optional)* The column's name in in DynamoDB. Defaults to the index’s name in the model.
    """
    def __init__(self, typedef, hash_key=False, range_key=False, dynamo_name=None, default=missing):
        self.hash_key: bool = hash_key
        self.range_key: bool = range_key
        self._name: str = None
        self._dynamo_name: str = dynamo_name

        if not callable(default):
            self.default = lambda: default
        else:
            self.default = default

        if subclassof(typedef, Type):
            typedef = typedef()
        if instanceof(typedef, Type):
            self.typedef = typedef

        else:
            raise TypeError(f"Expected {typedef} to be instance or subclass of Type")
        super().__init__()

    def __copy__(self):
        """
        Create a shallow copy of this Column.  Primarily used when initializing models that subclass other abstract
        models or mixins (baseless classes that contain Columns and Indexes).  You can override this method to
        change how derived models are created:

        .. code-block:: python

            import copy


            class MyColumn(Column):
                def __copy__(self):
                    new = super().__copy__()
                    new.derived = True
                    return new


            column = MyColumn(Integer)
            same = copy.copy(column)
            assert same.derived  # True

        :return: A shallow copy of this Column, with the ``model`` and ``_name`` attributes unset.
        """
        cls = self.__class__
        obj = cls.__new__(cls)
        obj.__dict__.update(self.__dict__)
        obj.model = None
        obj._name = None
        return obj

    def __set_name__(self, owner, name):
        self._name = name

    __hash__ = object.__hash__

    def __set__(self, obj, value):
        if self._name is None:
            raise AttributeError("Can't set field without binding to model")
        obj.__dict__[self._name] = value
        # Notify the tracking engine that this value was intentionally mutated
        object_modified.send(self, obj=obj, column=self, value=value)

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        if self._name is None:
            raise AttributeError("Can't get field without binding to model")
        try:
            return obj.__dict__[self._name]
        except KeyError:
            raise AttributeError(f"'{obj.__class__}' has no attribute '{self._name}'")

    def __delete__(self, obj):
        try:
            if self._name is None:
                raise AttributeError("Can't delete field without binding to model")
            try:
                del obj.__dict__[self._name]
            except KeyError:
                raise AttributeError(f"'{obj.__class__}' has no attribute '{self._name}'")
        finally:
            # Unlike set, we always want to mark on delete.  If we didn't, and the column wasn't loaded
            # (say from a query) then the intention "ensure this doesn't have a value" wouldn't be captured.
            object_modified.send(self, obj=obj, column=self, value=None)

    def __repr__(self):
        if self.hash_key:
            extra = "=hash"
        elif self.range_key:
            extra = "=range"
        else:
            extra = ""

        # <Column[Pin.url]>
        # <Column[User.id=hash]>
        # <Column[File.fragment=range]>
        return f"<{self.__class__.__name__}[{self.model.__name__}.{self.name}{extra}]>"

    @property
    def name(self):
        """Name of the model's attr that references self"""
        return self._name

    @property
    def dynamo_name(self):
        if self._dynamo_name is None:
            return self.name
        return self._dynamo_name
tests/helpers/models.py
class VectorModel(BaseModel):
    name = Column(String, hash_key=True)
    list_str = Column(List(String))
    set_str = Column(Set(String))
    map_nested = Column(Map(**{
        "bytes": Binary,
        "str": String,
        "map": Map(**{
            "int": Integer,
            "str": String
        })
    }))
    some_int = Column(Integer)
    some_bytes = Column(Binary)
bloop/conditions.py
class ObjectTracking(weakref.WeakKeyDictionary):
class ReferenceTracker:
class ConditionRenderer:
class BaseCondition:
class Condition(BaseCondition):
class AndCondition(BaseCondition):
class OrCondition(BaseCondition):
class NotCondition(BaseCondition):
class ComparisonCondition(BaseCondition):
class BeginsWithCondition(BaseCondition):
class BetweenCondition(BaseCondition):
class ContainsCondition(BaseCondition):
class InCondition(BaseCondition):
class ComparisonMixin:
class Proxy(ComparisonMixin):
    def __getitem__(self, key):
def on_object_modified(_, *, obj, column, **__):
def is_empty(ref):
    def __init__(self, engine):
    def next_index(self):
    def _name_ref(self, name):
    def _path_ref(self, column: "ComparisonMixin"):
    def _value_ref(self, column, value, *, inner=False):
    def any_ref(self, *, column, value=missing, inner=False) -> Reference:
    def pop_refs(self, *refs):
def render(engine, obj=None, filter=None, projection=None, key=None, condition=None, update=None):
    def __init__(self, engine):
    def render(self, obj=None, condition=None, update=False, filter=None, projection=None, key=None):
    def condition_expression(self, condition):
    def filter_expression(self, condition):
    def key_expression(self, condition):
    def projection_expression(self, columns):
    def update_expression(self, obj):
    def output(self):
    def __init__(self, operation, *, column=None, values=None):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __invert__(self):
    def __and__(self, other):
    def __iand__(self, other):
    def __or__(self, other):
    def __ior__(self, other):
    def __eq__(self, other):
    def __init__(self):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, *values):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, *values):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, value):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, operation, column, value):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, column, value):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, column, lower, upper):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, column, value):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __init__(self, column, values):
    def __len__(self):
    def __repr__(self):
    def render(self, renderer):
    def __repr__(self):
    def __getitem__(self, path):
    def __eq__(self, value):
    def __ne__(self, value):
    def __lt__(self, value):
    def __gt__(self, value):
    def __le__(self, value):
    def __ge__(self, value):
    def begins_with(self, value):
    def between(self, lower, upper):
    def contains(self, value):
    def in_(self, *values):
def check_support(column: ComparisonMixin, operation):
    def __init__(self, obj, path):
    def __getattr__(self, item):
    def __getitem__(self, item):
    def __repr__(self):
def printable_name(column, path=None):
def path_of(obj):
def proxied(obj):
def iter_conditions(condition):
def iter_columns(condition):


Question: import logging
import operator
import pytest
from bloop import actions
from bloop.conditions import (
    AndCondition,
    BaseCondition,
    BeginsWithCondition,
    BetweenCondition,
    ComparisonCondition,
    ComparisonMixin,
    Condition,
    ConditionRenderer,
    ContainsCondition,
    InCondition,
    InvalidCondition,
    NotCondition,
    OrCondition,
    Proxy,
    Reference,
    ReferenceTracker,
    global_tracking,
    iter_columns,
    iter_conditions,
    printable_name,
    render,
)
from bloop.models import BaseModel, Column
from bloop.types import Binary, Boolean, Integer, List, Map, Set, String
from ..helpers.models import Document, User, VectorModel
    assert global_tracking[user] == {User.id}

    # Even when the delete fails, the column is marked.
    # We're tracking intention, not state change.
    with pytest.raises(AttributeError):
        del user.age
    assert global_tracking[user] == {User.id, User.age}


# END TRACKING SIGNALS ========================================================================== END TRACKING SIGNALS


# REFERENCE TRACKER ================================================================================ REFERENCE TRACKER


def test_ref_index_always_increments(reference_tracker):
    """Don't risk forgetting to increment it - ALWAYS increment after getting."""
    assert reference_tracker.next_index == 0
    assert reference_tracker.next_index == 1


def test_ref_same_name(reference_tracker):
    """Don't create two references for the same name string"""
    name = "foo"
    expected_ref = "#n0"

    ref = reference_tracker._name_ref(name)
    same_ref = reference_tracker._name_ref(name)

    assert ref == same_ref == expected_ref
    assert reference_tracker.attr_names[ref] == name
    assert reference_tracker.name_attr_index[name] == ref
    assert reference_tracker.counts[ref] == 2


def test_ref_path_empty(reference_tracker):
    """Path reference without a path (column only) is just a name ref"""
    column = MockColumn("column")
    expected_name = "d_column"
    expected_ref = "#n0"

    ref = reference_tracker._path_ref(column)

    assert ref == expected_ref
    assert reference_tracker.attr_names[ref] == expected_name
    assert reference_tracker.name_attr_index[expected_name] == ref
    assert reference_tracker.counts[ref] == 1


def test_ref_path_complex(reference_tracker):
    """Path reference with integer and string indexes.  Strings include duplicates and literal periods."""
    column = MockColumn("column")["foo"][3][4]["repeat"]["has.period"]["repeat"]
    expected_ref = "#n0.#n1[3][4].#n2.#n3.#n2"
    expected_names = {
        "#n0": "d_column",
        "#n1": "foo",
        "#n2": "repeat",
        "#n3": "has.period"
    }

    ref = reference_tracker._path_ref(column)

    assert ref == expected_ref
    assert reference_tracker.attr_names == expected_names


def test_ref_path_reuse(reference_tracker):
    """paths are re-used, even across columns"""
    first = MockColumn("first")[3]["foo"]
    second = MockColumn("second")[3]["foo"]

    expected_first = "#n0[3].#n1"
    expected_second = "#n2[3].#n1"
    expected_names = {
        "#n0": "d_first",
        "#n1": "foo",
        "#n2": "d_second"
    }

    first_ref = reference_tracker._path_ref(first)
    second_ref = reference_tracker._path_ref(second)
    assert first_ref == expected_first
    assert second_ref == expected_second
    assert reference_tracker.attr_names == expected_names


def test_ref_path_periods(reference_tracker):
    """Path segments with periods aren't de-duped with each individual segment"""
    column = MockColumn("column")["foo"]["foo.bar"]["bar"]
    expected_ref = "#n0.#n1.#n2.#n3"
    expected_names = {
        "#n0": "d_column",
        "#n1": "foo",
        "#n2": "foo.bar",
        "#n3": "bar",
    }

    ref = reference_tracker._path_ref(column)

    assert ref == expected_ref
    assert reference_tracker.attr_names == expected_names


def test_ref_value(reference_tracker):
    """no path, value not dumped"""
    column = User.age
    value = 3
    expected_ref = ":v0"
    expected_action = actions.set({"N": "3"})
    expected_values = {":v0": expected_action.value}

    ref, action = reference_tracker._value_ref(column, value)

    assert ref == expected_ref
    assert action == expected_action
    assert reference_tracker.attr_values == expected_values


def test_ref_value_path(reference_tracker):
    """has path, value not dumped"""

Answer:     column = Document.data["Description"]["Body"]