Package: cruddy
# cruddy[![Circle CI](https://circleci.com/gh/cloudnative/cruddy.svg?style=svg)](https://circleci.com/gh/cloudnative/cruddy)A simple CRUD wrapper around Amazon DynamoDB.## Installation```$ pip install cruddy```## Getting StartedThe first thing to do is to create a CRUD handler for your DynamoDB table.  Theconstructor for the CRUD class takes a number of parameters to help configurethe handler for your application.  The full list of parameters are:* **table_name** - name of the backing DynamoDB table (required)* **profile_name** - name of the AWS credential profile to use when creating theboto3 Session* **region_name** - name of the AWS region to use when creating the boto3 Session* **prototype** - a dictionary that describes the prototypical object stored inyour table (see below)* **supported_ops** - a list of operations supported by the CRUD handler(choices are list, get, create, update, delete, search, increment_counter)* **encrypted_attributes** - a list of lists or tuples where the first item isthe name of the attribute that should be encrypted and the second item is theKMS master key ID to use for encrypting/decrypting the value.* **debug** - if not False this will cause the raw_response to be leftin the response dictionary### PrototypesA prototype is a description of the prototypical item in your table.  It'skind of like a template for the item.  A prototype can be used to describe whatattributes are in the item, which are required or optional, and the type ofvalue that is associated with the attribute.  In addition, there are specialvalues you can use that allow a small range of calculated values in your item.If you don't specify a prototype, cruddy will store whatever values are in theitem with no validation or insertion of calculated values.Let's look at a few examples using prototypes.```{'id': '','created_at': 1,'foo': 1}```This prototype says that your item must have an ``id`` attribute whose value isof type ``str``, a ``created_at`` attribute whose value is of type ``int``, anda ``foo`` attribute whose value is also an ``int``.  Your item may containother items as well (this is not a schema) but it must contain these attributename/value pairs.  If the item you pass into the ``create`` method does notcontain these attributes cruddy will create the necessary attributes and willinitialize the value to what ever value you have specified.#### Calculated ValuesThe above example assumes that you are going to generate the ``id`` and``created_at`` values in your application code.  You may, however, prefer tohave cruddy handle that for you.  In that case, you can make use of cruddy'scalcuated value tokens.```{'id': 'on-create:<uuid>','created_at': 'on-create:<timestamp>'}```Now, when you create a new item you could supply one without an ``id`` or``created_at`` value and cruddy will calculate these values for you.  If thoseattributes already exist in the item, cruddy will not overwrite them.  Notethat the calulated values are specified as ``on-create``.  This is called a``trigger`` and indicates when the calculation will be performed.If you wanted to also have a timestamp to indicate when an item has beenmodified (i.e. created or updated) you could do this.```{'id': 'on-create:<uuid>','created_at': 'on-create:<timestamp>','modified_at': 'on-update:<timestamp>'}```The currently supported calculated value types are:* **``<uuid>``** to generate a string representation of a Type4 UUID* **``<timestamp>``** to generate an integer timestamp generated by``int(time.time()*1000)``The currently supported triggers for calculated values are:* **on-create** will be applied when the item is created* **on-update** will be applied when the item is created or updated### Configuring your CRUD handlerAn easy way to configure your CRUD handler is to gather all of the parameterstogether in a dictionary and then pass that dictionary to the classconstructor.```import cruddyparams = {'profile_name': 'foobar','region_name': 'us-west-2','table_name': 'fiebaz','prototype': {'id': '<on-create:uuid>','created_at': '<on-create:timestamp>','modified_at': '<on-update:timestamp>'}}crud = cruddy.CRUD(**params)```Once you have your handler, you can start to use it.```item = {'name': 'the dude', 'email': 'the@dude.com', 'twitter': 'thedude'}response = crud.create(item)```The response returned from all CRUD operations is a Python object with thefollowing attributes.* **data** is the actual data returned from the CRUD operation (if successful)* **status** is the status of the response and is either ``success`` or``error``* **metadata** is metadata from the underlying DynamoDB API call* **error_type** will be the type of error, if ``status != 'success'``* **error_code** will be the code of error, if ``status != 'success'``* **error_type** will be the full error message, if ``status != 'success'``* **raw_response** will contain the full response from DynamoDB if the CRUDhandler is in ``debug`` mode.* **is_successful** a simple short-cut, equivalent to ``status == 'success'``You can convert the CRUDResponse object into a standard Python dictionary usingthe ``flatten`` method```>>> response = crud.create(...)>>> response.flatten(){'data': {'created_at': 1452109758363,'name': 'the dude','email': 'the@dude.com','twitter': 'thedude','id': 'a6ac0fd7-cdde-4170-a1a9-30e139c44897','modified_at': 1452109758363},'error_code': None,'error_message': None,'error_type': None,'metadata': {'HTTPStatusCode': 200,'RequestId': 'LBBFLMIAVOKR8LOTK7SRGFO4Q3VV4KQNSO5AEMVJF66Q9ASUAAJG'},'raw_response': None,'status': 'success'}>>>```## CRUD operationsThe CRUD object supports the following operations.  Note that depending on thevalue of the ``supported_operations`` parameter passed to the constructor, someof these methods may return an ``UnsupportedOperation`` error type.### list()Returns a list of items in the database.  Encrypted attributes are notdecrypted when listing items.### get(*id*, *decrypt=False*)Returns the item corresponding to ``id``.  If the ``decrypt`` param is notFalse (the default) any encrypted attributes in the item will be decryptedbefore the item is returned.  If not, the encrypted attributes will contain theencrypted value.### create(*item*)Creates a new item.  You pass in an item containing initial values.  Anyattribute names defined in ``prototype`` that are missing from the item will beadded using the default value defined in ``prototype``.### update(*item*, *encrypt=True*)Updates the item based on the current values of the dictionary passed in. Ifthe ``encrypt`` param is True (the default), encrypted attributes in ``item``are encrypted. To prevent double-encrypting when using ``list`` or ``get``without ``decrypt=True``, you can specify ``encrypt=False`` and the item willbe stored verbatim.### delete(*id*)Deletes the item corresponding to ``id``.## Beyond CRUDThe following operations extend beyond the basic CRUD functions but areincluded because of they are quite useful.### search(*query*)Cruddy provides a limited but useful interface to search GSI indexes in DynamoDBwith the following limitations (hopefully some of these will be expanded oreliminated in the future.* The GSI must be configured with a only HASH and not a RANGE.* The only operation supported in the query is equalityTo use the ``search`` operation you must pass in a query string of this form:<attribute_name>=<value>As stated above, the only operation currently supported is equality (=) butother operations will be added over time.  Also, the ``attribute_name`` must bean attribute which is configured as the ``HASH`` of a GSI in the DynamoDBtable.  If all of the above conditions are met, the ``query`` operation willreturn a list (possibly empty) of all items matching the query and the``status`` of the response will be ``success``.  Otherwise, the ``status`` willbe ``error`` and the ``error_type`` and ``error_message`` will provide furtherinformation about the error.### increment_counter(*id*, *counter_name*, [*increment*])Atomically increments a counter attribute in the item identified by ``id``.  You must specify thename of the attribute as ``counter_name`` and, optionally, the ``increment``which defaults to ``1``.## Using the handler interfaceIn addition to the methods described above, cruddy also provides a generichandler interface.  This is mainly useful when you want to wrap a cruddyhandler in a Lambda function and then call that Lambda function to access theCRUD capabilities.To call the handler, you simply put all necessary parameters into a Pythondictionary and then call the handler with that dict.```params = {'operation': 'create','item': {'foo': 'bar', 'fie': 'baz'}}response = crud.handler(**params)```So, you could define a Lambda function like this:```import loggingimport jsonimport cruddyLOG = logging.getLogger()LOG.setLevel(logging.INFO)config = json.load(open('config.json'))crud = cruddy.CRUD(**config)def handler(event, context):LOG.info(event)response = crud.handler(**event)return response.flatten()```Where ``config.json`` looks like this:```{"region_name": "us-west-2","table_name": "foobar","prototype": {"id": "<on-create:uuid>","created_at": "<on-create:timestamp>","modified_at": "<on-update:timestamp>","foo": 1,"bar": ""},}```If you uploaded this function (and config file) to AWS Lambda you could theninvoke the handler like this.```import jsonimport boto3session = boto3.Session()lambda_client = session.client('lambda')params = {'operation': 'create', 'item': {'fie': 'baz'}}response = lambda_client.invoke(FunctionName='myfunction',InvocationType='RequestResponse',Payload=json.dumps(params))cruddy_response = json.load(response['Payload'])```The variable ``cruddy_response`` would now contain the response structurereturned by cruddy, flattened into a Python dictionary.## The cruddy CLIcruddy also offers a CLI that allows you to access your DynamoDB table orLambda-based handler via a simple command line interface.  It supports alloperations supported by cruddy.### Using the cruddy CLI with a DynamoDB tableTo use cruddy to directly interact with a DynamoDB table, you need to place theconfiguration information for your cruddy handler in a JSON file.  So, from ourprevious example if we created a file called ``fiebaz.json`` like this:```{"profile_name": "foobar","region_name": "us-west-2","table_name": "fiebaz","prototype": {"id": "<on-create:uuid>","created_at": "<on-create:timestamp>","modified_at": "<on-update:timestamp>"}}```We could then reference this when using the cruddy CLI:```$ cruddy --config-file fiebaz.json list[{<a listing of all items in fiebaz>}...]```Use the ``--help`` for more information on how to use the cruddy CLI.### Using the cruddy CLI with a Lambda handlerAll of the operations of the CLI work exactly the same whether you are using itwith a DynamoDB table directly or through a Lambda controller.  The onlydifference is that rather than referencing a config file containing info aboutthe table and other parameters needed to create the cruddy CRUD handler, yousimply tell the CLI about the Lambda function.```$ cruddy --lambda-fn fiebaz list[{<a listing of all items in fiebaz>}...]```where ``fiebaz`` is the name of your Lambda handler.