.. _api-public:

Public
^^^^^^

======
Engine
======

By default, Bloop will build clients directly from :func:`boto3.client`.
To customize the engine's connection, you can provide your own DynamoDB and DynamoDBStreams clients:

.. code-block:: python

    import bloop
    import boto3

    dynamodb_local = boto3.client("dynamodb", endpoint_url="http://127.0.0.1:8000")
    streams_local = boto3.client("dynamodbstreams", endpoint_url="http://127.0.0.1:8001")

    engine = bloop.Engine(
        dynamodb=dynamodb_local,
        dynamodbstreams=streams_local)

.. autoclass:: bloop.engine.Engine
    :members:

======
Models
======

See :ref:`defining models <define-models>` in the User Guide.

---------
BaseModel
---------

.. autoclass:: bloop.models.BaseModel

    .. attribute:: Meta

        Holds table configuration and computed properties of the model.
        See :ref:`model meta <user-model-meta>` in the User Guide.

------
Column
------

.. autoclass:: bloop.models.Column
    :members:

    .. attribute:: dynamo_name

        The name of this column in DynamoDB.  Defaults to the column's
        :data:`~Column.model_name`.

    .. attribute:: hash_key

        True if this is the model's hash key.

    .. attribute:: model

        The model this column is attached to.

    .. attribute:: model_name

        The name of this column in the model.  Not settable.

        .. code-block:: pycon

            >>> class Document(BaseModel):
            ...     ...
            ...     cheat_codes = Column(Set(String), name="cc")
            ...
            >>> Document.cheat_codes.model_name
            cheat_codes
            >>> Document.cheat_codes.dynamo_name
            cc

    .. attribute:: range_key

        True if this is the model's range key.

--------------------
GlobalSecondaryIndex
--------------------

.. autoclass:: bloop.models.GlobalSecondaryIndex

    .. attribute:: dynamo_name

        The name of this index in DynamoDB.  Defaults to the index's
        :data:`~GlobalSecondaryIndex.model_name`.

    .. attribute:: hash_key

        The column that the index can be queried against.

    .. attribute:: model

        The model this index is attached to.

    .. attribute:: model_name

        The name of this index in the model.  Not settable.

        .. code-block:: pycon

            >>> class Document(BaseModel):
            ...     ...
            ...     by_email = GlobalSecondaryIndex(
            ...         projection="keys", name="ind_e", hash_key="email")
            ...
            >>> Document.by_email.model_name
            by_email
            >>> Document.by_email.dynamo_name
            ind_e

    .. attribute:: projection

        .. code-block:: python

            {
                "available":  # Set of columns that can be returned from a query or search.
                "included":   # Set of columns that can be used in query and scan filters.
                "mode":       # "all", "keys", or "include"
                "strict":     # False if queries and scans can fetch non-included columns
            }

        GSIs can't incur extra reads, so "strict" will always be true and "available" is always the same as "included".

    .. attribute:: range_key

        The column that the index can be sorted on.  May be ``None``.

    .. attribute:: read_units

        Provisioned read units for the index.  GSIs have their own provisioned throughput.

    .. attribute:: write_units

        Provisioned write units for the index.  GSIs have their own provisioned throughput.

-------------------
LocalSecondaryIndex
-------------------

.. autoclass:: bloop.models.LocalSecondaryIndex

    .. attribute:: dynamo_name

        The name of this index in DynamoDB.  Defaults to the index's
        :data:`~LocalSecondaryIndex.model_name`.

    .. attribute:: hash_key

        LSI's hash_key is always the table hash_key.

    .. attribute:: model

        The model this index is attached to.

    .. attribute:: model_name

        The name of this index in the model.  Not settable.

        .. code-block:: pycon

            >>> class Document(BaseModel):
            ...     ...
            ...     by_date = LocalSecondaryIndex(
            ...         projection="keys", name="ind_co", range_key="created_on")
            ...
            >>> Document.by_date.model_name
            by_date
            >>> Document.by_date.dynamo_name
            ind_co

    .. attribute:: projection

        .. code-block:: python

            {
                "available":  # Set of columns that can be returned from a query or search.
                "included":   # Set of columns that can be used in query and scan filters.
                "mode":       # "all", "keys", or "include"
                "strict":     # False if queries and scans can fetch non-included columns
            }

        LSIs can incur extra reads, so "available" may be a superset of "included".

    .. attribute:: range_key

        The column that the index can be sorted on.  LSIs always have a range_key.

    .. attribute:: read_units

        Provisioned read units for the index.  LSIs share the table's provisioned throughput.

    .. attribute:: write_units

        Provisioned write units for the index.  LSIs share the table's provisioned throughput.

.. _public-types:

=====
Types
=====

Most custom types only need to specify a backing_type (or subclass a built-in type) and override
:func:`~bloop.types.Type.dynamo_dump` and :func:`~bloop.types.Type.dynamo_load`:

.. code-block:: python

    class ReversedString(Type):
        python_type = str
        backing_type = "S"

        def dynamo_load(self, value, *, context, **kwargs):
            return str(value[::-1])

        def dynamo_dump(self, value, *, context, **kwargs):
            return str(value[::-1])

If a type's constructor doesn't have required args, a :class:`~bloop.models.Column` can use the class directly.
The column will create an instance of the type by calling the constructor without any args:

.. code-block:: python

    class SomeModel(BaseModel):
        custom_hash_key = Column(ReversedString, hash_key=True)

In rare cases, complex types may need to implement :func:`~bloop.types.Type._dump`,
:func:`~bloop.types.Type._load`, or :func:`~bloop.types.Type._register`.

----
Type
----

.. autoclass:: bloop.types.Type
    :members: dynamo_dump, dynamo_load, _dump, _load, _register
    :member-order: bysource

    .. attribute:: python_type

        The type local values will have.  Informational only, this is not used for validation.

    .. attribute:: backing_type

        The DynamoDB type that Bloop will store values as.

        One of:

        .. hlist::
            :columns: 3

            * ``"S"`` -- string
            * ``"N"`` -- number
            * ``"B"`` -- binary
            * ``"SS"`` -- string set
            * ``"NS"`` -- number set
            * ``"BS"`` -- binary set
            * ``"M"`` -- map
            * ``"L"`` -- list
            * ``"BOOL"`` -- boolean

        See the `DynamoDB API Reference`__ for details.

        __ http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html

------
String
------

.. autoclass:: bloop.types.String

    .. attribute:: backing_type
        :annotation: = "S"

    .. attribute:: python_type
        :annotation: = str

.. _api-public-number:

------
Number
------

You should use :class:`decimal.Decimal` instances to avoid rounding errors:

.. code-block:: pycon

    >>> from bloop import BaseModel, Engine, Column, Number, Integer
    >>> class Product(BaseModel):
    ...     id = Column(Integer, hash_key=True)
    ...     rating = Column(Number)

    >>> engine = Engine()
    >>> engine.bind(Rating)

    >>> product = Product(id=0, rating=3.14)
    >>> engine.save(product)
    # Long traceback
    Inexact: [<class 'decimal.Inexact'>, <class 'decimal.Rounded'>]

    >>> from decimal import Decimal as D
    >>> product.rating = D('3.14')
    >>> engine.save(product)
    >>> # Success!

.. autoclass:: bloop.types.Number

    .. seealso::

        If you don't want to deal with :class:`decimal.Decimal`\, see the
        :ref:`Float <patterns-float>` type in the patterns section.

    .. attribute:: backing_type
        :annotation: = "N"

    .. attribute:: python_type
        :annotation: = decimal.Decimal

    .. attribute:: context
        :annotation: = decimal.Context

        The context used to transfer numbers to DynamoDB.

------
Binary
------

.. autoclass:: bloop.types.Binary

    .. attribute:: backing_type
        :annotation: = "B"

    .. attribute:: python_type
        :annotation: = bytes

-------
Boolean
-------

.. autoclass:: bloop.types.Boolean

    .. attribute:: backing_type
        :annotation: = "BOOL"

    .. attribute:: python_type
        :annotation: = bool

----
UUID
----

.. autoclass:: bloop.types.UUID

    .. attribute:: backing_type
        :annotation: = "S"

    .. attribute:: python_type
        :annotation: = uuid.UUID

--------
DateTime
--------

.. autoclass:: bloop.types.DateTime

    .. attribute:: backing_type
        :annotation: = "S"

    .. attribute:: python_type
        :annotation: = arrow.Arrow

    .. attribute:: timezone
        :annotation: = str

        The timezone used for local values.

-------
Integer
-------

.. autoclass:: bloop.types.Integer

    .. attribute:: backing_type
        :annotation: = "N"

    .. attribute:: python_type
        :annotation: = int

    .. attribute:: context
        :annotation: = decimal.Context

        The context used to transfer numbers to DynamoDB.

---
Set
---

.. autoclass:: bloop.types.Set

    .. attribute:: backing_type
        :annotation: = "SS", "NS", or "BS"

        Set is not a standalone type; its backing type depends on the inner type its constructor receives. For
        example, ``Set(DateTime)`` has backing type "SS" because :class:`~bloop.types.DateTime` has backing type "S".

    .. attribute:: python_type
        :annotation: = set

    .. attribute:: inner_typedef
        :annotation: = Type

        The typedef for values in this Set.  Has a backing type of "S", "N", or "B".

----
List
----

.. autoclass:: bloop.types.List

    .. attribute:: backing_type
        :annotation: = "L"

    .. attribute:: python_type
        :annotation: = list

    .. attribute:: inner_typedef
        :annotation: = Type

        The typedef for values in this List.  All types supported.

---
Map
---

.. autoclass:: bloop.types.Map

    .. attribute:: backing_type
        :annotation: = "M"

    .. attribute:: python_type
        :annotation: = dict

    .. attribute:: types
        :annotation: = dict

        Specifies the Type for each key in the Map.  For example, a Map with two keys "id" and "rating" that are
        a UUID and Number respectively would have the following types:

        .. code-block:: python

            {
                "id": UUID(),
                "rating": Number()
            }

=====
Query
=====

.. autoclass:: bloop.search.QueryIterator

    .. attribute:: count

        Number of items that have been loaded from DynamoDB so far, including buffered items.

    .. attribute:: exhausted

        True if there are no more results.

    .. function:: first()

        Return the first result.  If there are no results, raises :exc:`~bloop.exceptions.ConstraintViolation`.

    .. function:: one()

        Return the unique result.  If there is not exactly one result,
        raises :exc:`~bloop.exceptions.ConstraintViolation`.

    .. function:: reset()

        Reset to the initial state, clearing the buffer and zeroing count and scanned.

    .. attribute:: scanned

        Number of items that DynamoDB evaluated, before any filter was applied.

====
Scan
====

.. autoclass:: bloop.search.ScanIterator

    .. attribute:: count

        Number of items that have been loaded from DynamoDB so far, including buffered items.

    .. attribute:: exhausted

        True if there are no more results.

    .. function:: first()

        Return the first result.  If there are no results, raises :exc:`~bloop.exceptions.ConstraintViolation`.

    .. function:: one()

        Return the unique result.  If there is not exactly one result,
        raises :exc:`~bloop.exceptions.ConstraintViolation`.

    .. function:: reset()

        Reset to the initial state, clearing the buffer and zeroing count and scanned.

    .. attribute:: scanned

        Number of items that DynamoDB evaluated, before any filter was applied.

======
Stream
======

:func:`Engine.stream() <bloop.engine.Engine.stream>` is the recommended way to create a stream.
If you manually create a stream, you will need to call :func:`~bloop.stream.Stream.move_to` before iterating the
Stream.

.. warning::

    **Chronological order is not guaranteed for high throughput streams.**

    DynamoDB guarantees ordering:

    * within any single shard
    * across shards for a single hash/range key

    There is no way to exactly order records from adjacent shards.  High throughput streams
    provide approximate ordering using each record's "ApproximateCreationDateTime".

    Tables with a single partition guarantee order across all records.

    See :ref:`Stream Internals <internal-streams>` for details.

.. autoclass:: bloop.stream.Stream
    :members:

==========
Conditions
==========

The only public class the conditions system exposes is the empty condition, :class:`~bloop.conditions.Condition`.
The rest of the conditions system is baked into :class:`~bloop.models.Column` and consumed by the various
:class:`~bloop.engine.Engine` functions like :func:`Engine.save() <bloop.engine.Engine.save>`.

This function creates a condition for any model that can be used when saving to ensure you don't overwrite an existing
value.  The model's ``Meta`` attribute describes the required keys:

.. code-block:: python

    from bloop import Condition

    def ensure_unique(model):
        condition = Condition()
        for key in model.Meta.keys:
            condition &= key.is_(None)
        return condition

.. seealso::

    :ref:`conditions` in the User Guide describes the possible conditions, and when and how to use them.

.. autoclass:: bloop.conditions.Condition

.. _public-signals:

=======
Signals
=======

.. autodata:: bloop.signals.before_create_table
    :annotation:

.. autodata:: bloop.signals.object_loaded
    :annotation:

.. autodata:: bloop.signals.object_saved
    :annotation:

.. autodata:: bloop.signals.object_deleted
    :annotation:

.. autodata:: bloop.signals.object_modified
    :annotation:

.. autodata:: bloop.signals.model_bound
    :annotation:

.. autodata:: bloop.signals.model_created
    :annotation:

.. autodata:: bloop.signals.model_validated
    :annotation:

==========
Exceptions
==========

Except to configure sessions, Bloop aims to completely abstract the boto3/botocore layers.  If you encounter an
exception from either boto3 or botocore, please `open an issue`__.  Bloop's exceptions are broadly divided into two
categories: unexpected state, and invalid input.

To catch any exception from Bloop, use :exc:`~bloop.exceptions.BloopException`:

.. code-block:: python

    try:
        engine.stream(User, "latest")
    except BloopException:
        print("Didn't expect an exception, but Bloop raised:")
        raise

.. autoclass:: bloop.exceptions.BloopException

__ https://github.com/numberoverzero/bloop/issues/new

----------------
Unexpected state
----------------

These are exceptions that you should be ready to handle in the normal course of using DynamoDB.  For example,
failing to load objects will raise :exc:`~bloop.exceptions.MissingObjects`, while conditional operations may
fail with :exc`~bloop.exceptions.ConstraintViolation`.

.. autoclass:: bloop.exceptions.ConstraintViolation

.. autoclass:: bloop.exceptions.MissingObjects

.. autoclass:: bloop.exceptions.RecordsExpired

.. autoclass:: bloop.exceptions.ShardIteratorExpired

.. autoclass:: bloop.exceptions.TableMismatch

---------
Bad Input
---------

These are thrown when an option is invalid or missing, such as forgetting a key condition for a query,
or trying to use an unknown projection type.

.. autoclass:: bloop.exceptions.InvalidComparisonOperator

.. autoclass:: bloop.exceptions.InvalidCondition

.. autoclass:: bloop.exceptions.InvalidFilterCondition

.. autoclass:: bloop.exceptions.InvalidIndex

.. autoclass:: bloop.exceptions.InvalidKeyCondition

.. autoclass:: bloop.exceptions.InvalidModel

.. autoclass:: bloop.exceptions.InvalidPosition

.. autoclass:: bloop.exceptions.InvalidProjection

.. autoclass:: bloop.exceptions.InvalidSearchMode

.. autoclass:: bloop.exceptions.InvalidShardIterator

.. autoclass:: bloop.exceptions.InvalidStream

.. autoclass:: bloop.exceptions.MissingKey

.. autoclass:: bloop.exceptions.UnboundModel

.. autoclass:: bloop.exceptions.UnknownType