####################### Extending DynamoDB-mock ####################### Get the source Luke =================== :: $ hg clone ssh://hg@bitbucket.org/Ludia/dynamodb-mock $ pip install nose nosexcover coverage mock webtests boto $ python setup.py develop $ nosetests # --no-skip to run boto integration tests too Folder structure ================ :: DynamoDB-Mock +-- ddbmock | +-- database => request engine | | `-- storage => storage backend | +-- operations => each DynamoDB operation has a route here | +-- router => entry-points logic | `-- validators => request syntax validation middleware +-- docs | `-- pages `-- tests +-- unit => mainly details and corner cases tests `-- functional +-- boto => main/extensive tests `-- pyramid => just make sure that all methods are supported Request flow: the big picture ============================= .. figure:: ../_static/archi.png :align: center Global request flow Just a couple of comments here: - The ``router`` relies on introspection to find the validators (if any) - The ``router`` relies on introspection to find the routes - The ``database engine`` relies on introspection to find the configured storage backend - There is a "catch all" in the router that maps to DynamoDB internal server error Adding a custom action ====================== As long as the method follows DynamoDB request structure, it is mostly a matter of adding a file to ``ddbmock/routes`` with the following conventions: - file_name: "underscore" version of the camel case method name. - function_name: file_name. - argument: parsed post payload. - return: response dict. Example: adding a ``HelloWorld`` method: :: # -*- coding: utf-8 -*- # module: ddbmock.routes.hello_world.py def hello_world(post): return { 'Hello': 'World' } If the ``post`` of your method contains ``TableName``, you may auto-load the corresponding table this way: :: # -*- coding: utf-8 -*- # module: ddbmock.routes.hello_world.py from ddbmock.utils import load_table() @load_table def hello_world(post): return { 'Hello': 'World' } Adding a validator ================== Let's say you want to let your new ``HelloWorld`` greet someone in particular, you will want to add an argument to the request. Example: simplest way to add support for an argument: :: # -*- coding: utf-8 -*- # module: ddbmock.routes.hello_world.py def hello_world(post): return { 'Hello': 'World (and "{you}" too!)'.format(you=post['Name'] } Wanna test it? >>> curl -d '{"Name": "chuck"}' -H 'x-amz-target: DynamoDB_custom.HelloWorld' localhost:6543 {'Hello': 'World (and "chuck" too!)'} But this is most likely to crash the server if 'Name' is not in ``Post``. This is where ``Voluptuous`` comes. In ddbmock, all you need to do to enable automatic validations is to add a file with the underscore name in ``ddbmock.validators``. It must contain a ``post`` member with the rules. Example: HelloWorld validator for HelloWorld method: :: # -*- coding: utf-8 -*- # module: ddbmock.validators.hello_world.py post = { u'Name': unicode, } Done ! Adding a storage backend ======================== Storage backends lives in 'ddbmock/database/storage'. There are currently two of them built-in. Basic "in-memory" (default) and "sqlite" to add persistence. As for the methods, storage backends follow conventions to keep the code lean - they must be in ``ddbmock.database.storage`` module - they must implement ``Store`` class following this outline :: # -*- coding: utf-8 -*- # in case you need to load configuration constants from ddbmock import config # the name can *not* be changed. class Store(object): def __init__(self, name): """ Initialize the in-memory store :param name: Table name. """ # TODO def truncate(self): """Perform a full table cleanup. Might be a good idea in tests :)""" # TODO def __getitem__(self, (hash_key, range_key)): """Get item at (``hash_key``, ``range_key``) or the dict at ``hash_key`` if ``range_key`` is None. :param key: (``hash_key``, ``range_key``) Tuple. If ``range_key`` is None, all keys under ``hash_key`` are returned :return: Item or item dict :raise: KeyError """ # TODO def __setitem__(self, (hash_key, range_key), item): """Set the item at (``hash_key``, ``range_key``). Both keys must be defined and valid. By convention, ``range_key`` may be ``False`` to indicate a ``hash_key`` only key. :param key: (``hash_key``, ``range_key``) Tuple. :param item: the actual ``Item`` data structure to store """ # TODO def __delitem__(self, (hash_key, range_key)): """Delete item at key (``hash_key``, ``range_key``) :raises: KeyError if not found """ # TODO def __iter__(self): """ Iterate all over the table, abstracting the ``hash_key`` and ``range_key`` complexity. Mostly used for ``Scan`` implementation. """ # TODO As an example, I recommend to study "memory.py" implementation. It is pretty straight-forward and well commented. You get the whole package for only 63 lines :)