==========
User guide
==========
Introduction
============
Hopefully the :ref:`quick-start` was enough to get you started.
I do my best to respect standards, so alternative installation methods like ``easy_install LowVoltage`` or ``python setup.py install`` in the source code should work as well as ``pip install LowVoltage``.
You may want to use `virtualenv `__.
Regarding imports, I suggest you ``import LowVoltage as lv`` in your production code.
All this documentation assumes that everything was imported in the global namespace: ``from LowVoltage import *``.
Note that everything is available in the top-level module, so you don't have to import nested modules.
Beware that LowVoltage is just a client library for DynamoDB, so you have to be familiar with the underlying concepts of this No-SQL database.
AWS provides `a lot of documentation `__ and this documentation often links to it.
I'd like all communication about LowVoltage to be public so please use `GitHub issues `__ for your questions and remarks as well as for reporting bugs.
Why?
====
- I wanted to learn DynamoDB
- I found out Boto is (was?) not up-to-date with newer API parameters and does (did?) not support Python 3
- I had some time and I love programming
.. _tenets:
Tenets
======
Users should be able to do everything that is permited by `the API `__.
--------------------------------------------------------------------------------------------------------------------------------------
There is nothing worse (well, maybe there is...) than knowing how to do something using an API but being incapacited by a client library.
So in LowVoltage we provide a first layer, :ref:`actions`, that maps exactly to the API.
Please `open an issue `__ if something is missing!
Users should never risk a typo
------------------------------
We provide symbols for all DynamoDB constants.
Users should be able to choose simplicity over flexibility
----------------------------------------------------------
Even if you *want* to be able to :meth:`~.PutItem.return_item_collection_metrics_size`, most of the time you don't care. And processing :attr:`~.BatchWriteItemResponse.unprocessed_items` should be abstracted away. So we provide :ref:`compounds`. More details in :ref:`actions-vs-compounds` bellow.
The connection
==============
The main entry point of LowVoltage is the :class:`.Connection` class.
>>> from LowVoltage import *
>>> connection = Connection("us-west-2", EnvironmentCredentials())
Authentication
--------------
DynamoDB requires authentication and signing of the requests.
In LowVoltage we use simple dependency injection of a credentials provider (:class:`.EnvironmentCredentials` in the example above).
This approach is flexible and allows implementation of any credentials rotation policy.
LowVoltage comes with credentials providers for simple use cases.
See :mod:`.credentials` for details.
Error handling
--------------
Some errors are retryable: :exc:`.NetworkError` or :exc:`.ServerError` for example.
The :class:`.Connection` accepts a ``retry_policy`` parameter to specify this behavior.
See :mod:`.retry_policies` for details.
See also :mod:`.exceptions` for a description of the exceptions classes.
.. _actions-vs-compounds:
Actions vs. compounds
=====================
As briefly described in the :ref:`tenets`, LowVoltage is built in successive layers of increasing abstraction and decreasing flexibility.
The :ref:`actions` layer provides almost no abstraction over `the API `__.
It maps exactly to the DynamoDB actions, parameters and return values.
It does convert between :ref:`python-types` and DynamoDB notation though.
>>> table = "LowVoltage.Tests.Doc.1"
>>> connection(GetItem(table, {"h": 0})).item
{u'h': 0, u'gr': 10, u'gh': 0}
The :ref:`compounds` layer provides helper functions that intend to complete actions in their simplest use cases.
For example :class:`.BatchGetItem` is limited to get 100 keys at once and requires processing :attr:`.BatchGetItemResponse.unprocessed_keys`, so we provide :func:`.iterate_batch_get_item` to do that.
The tradeoff is that you loose :attr:`.BatchGetItemResponse.consumed_capacity` and the ability to get items from several tables at once.
Similarly :func:`.batch_put_item` removes the limit of 25 items in :class:`.BatchWriteItem` but also removes the ability to put and delete from several tables in the same action.
>>> batch_put_item(connection, table, {"h": 0, "a": 42}, {"h": 1, "b": 53})
Actions are instances that are *passed to* the :class:`.Connection` but compounds are functions that *receive* the connection as an argument:
actions are atomic while compounds are able to perform several actions.
Someday, maybe, we'll write a Table abstraction and implement an "active record" pattern? It would be even simpler than compounds, but less flexible.
.. _building-actions:
Building actions
================
DynamoDB actions typically receive a lot of mandatory and optional parameters.
When you build an action, you *can* pass all mandatory parameters to the constructor.
You may want to use named parameters to reduce the risk of giving them in the wrong order.
>>> GetItem("Table", {"h": 0})
Optional parameters are exposed only using `method chaining `__ to avoid giving them in the wrong order.
>>> GetItem("Table", {"h": 0}).return_consumed_capacity_total()
Alternatively, mandatory parameters can be set using method chaining as well.
>>> GetItem().table_name("Table").key({"h": 0})
If you try to pass an action with missing mandatory parameters, you'll get a :exc:`.BuilderError`:
>>> connection(GetItem().key({"h": 0}))
Traceback (most recent call last):
...
BuilderError: ...
Active resource
---------------
Some actions can operate on several resources.
:class:`.BatchGetItem` can get items from several tables at once for example.
To build those, we need a concept of "active table": :meth:`.BatchGetItem.keys` will always add keys to get from the active table.
The active table is set by :meth:`.BatchGetItem.table`.
>>> (BatchGetItem()
... .table("Table1").keys({"h": 0})
... .table("Table2").keys({"x": 42})
... )
The previous example will get ``{"h": 0}`` from ``Table1`` and ``{"x": 42}`` from ``Table2``.
.. _variadic-functions:
Variadic functions
------------------
Some methods, like :meth:`.BatchGetItem.keys` are variadic.
But a special kind of variadic: not only do they accept any number of parameters, but for greater flexibility those arguments can also be iterable.
>>> (BatchGetItem().table("Table1")
... .keys({"h": 0})
... .keys({"h": 1}, {"h": 2})
... .keys({"h": 3}, [{"h": 4}, {"h": 5}], {"h": 6}, [{"h": 7}])
... .keys({"h": h} for h in range(8, 12))
... .keys(({"h": h} for h in range(12, 17)), {"h": 17}, [{"h": h} for h in range(18, 20)])
... )
Expressions
===========
@todoc Condition, projection, attribute names, attribute values.
@todoc Cross link here, next gen mixins and expression builders.