mirror of
https://github.com/sprockets/sprockets.clients.dynamodb.git
synced 2024-11-23 11:19:54 +00:00
Merge pull request #2 from sprockets/merge-tornado-dynamodb
Merge tornado dynamodb
This commit is contained in:
commit
5a08f214f7
12 changed files with 1793 additions and 33 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ dist
|
|||
env
|
||||
*.egg-info
|
||||
.coverage
|
||||
.idea
|
||||
|
|
21
.travis.yml
21
.travis.yml
|
@ -1,14 +1,33 @@
|
|||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
before_install:
|
||||
- pip install nose coverage codecov
|
||||
- docker pull tray/dynamodb-local
|
||||
- docker run -d -p 7777:7777 tray/dynamodb-local -inMemory -port 7777
|
||||
- mkdir /home/travis/.aws
|
||||
- printf "[default]\nregion=us-east-1\noutput=json\n" > /home/travis/.aws/config
|
||||
- printf "[default]\naws_access_key_id = FAKE0000000000000000\naws_secret_access_key = FAKE000000000000000000000000000000000000\n" > /home/travis/.aws/credentials
|
||||
- pip install -r requires/testing.txt
|
||||
install:
|
||||
- pip install -e .
|
||||
env:
|
||||
DYNAMODB_ENDPOINT: http://localhost:7777
|
||||
script: nosetests --with-coverage
|
||||
after_success:
|
||||
- codecov
|
||||
sudo: false
|
||||
deploy:
|
||||
distributions: sdist bdist_wheel
|
||||
provider: pypi
|
||||
user: sprockets
|
||||
on:
|
||||
python: 3.5
|
||||
tags: true
|
||||
all_branches: true
|
||||
password:
|
||||
secure: "pCvF0ROHU/p+mDgZT40yoRdNUmpov5B1jUh7mJ6bAUlsMNEaugX/cL+cUGNLgIhrcwBF93B7kdfuhGjO/2uF+k8aPhPocewwJ9qPTTyNMLGjpIclWp56KH9KLNISGmeTPguw06bpV0xOUw40AvSfTw4nmf4jaZsx1Ai2DUuoji7m1OvXwLL5+zXclngmxF7zVvPTnKmPDbJWUsF3n4DEJml8GBr7NW92yIo0Zu1LG3AiNrZWBebWa58Uv/DKOHQXYgyK0j3EixzTPkptoQgAByA6OVPPh6UOE2GUXuV83vDKeciyr/AExLQnlIaONa2FS4utOFdu2zoLsUJy+jeCJxVZ5D+jfYXSx1LyeQKjOZikUKNhI3O3XH7IYwd2YqhlRAE6SvFGQB1nYn6mXklSwdyOEaQ0ufUY4aCH9PRvswOUDJKIJw4xsiEUF46enrGWHVCnW3l0fPbhPx1GbB/PfzcJS3WSEgOKHbZ2u7PHrIkElxAOrI6Vabmrr0g5GD1T2DqBls600lQ/+HkRQ9cXVjegiUach3xj3IKL/gZJUuqiwl2xMPdIfi33GsZp5OItSt1fNmBZo4gz5zBEXYShpgeqx0hP0XEfQWnDLNoaHNhzaW9d1PYs7JHIsiLRw9HcJNdRzm4u08442m42WyEP5i3XnpmylFu6U+2a1mR4VEg="
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
include LICENSE
|
||||
include tests.py
|
||||
graft docs
|
||||
graft examples
|
||||
graft requires
|
||||
graft tests
|
||||
|
|
80
bootstrap
Executable file
80
bootstrap
Executable file
|
@ -0,0 +1,80 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# NAME
|
||||
# bootstrap -- initialize/update docker environment
|
||||
#
|
||||
# SYNOPSIS
|
||||
# bootstrap
|
||||
# bootstrap shellinit
|
||||
#
|
||||
# DESCRIPTION
|
||||
# Execute this script without parameters to build the local docker
|
||||
# environment. Once bootstrapped, dependent services are running
|
||||
# via docker-compose and the environment variables are written to
|
||||
# *build/test-environment* for future use.
|
||||
#
|
||||
# Running this script with the _shellinit_ command line parameter
|
||||
# causes it to simply interrogate the running docker environment,
|
||||
# update *build/test-environment*, and print the environment to
|
||||
# the standard output stream in a shell executable manner. This
|
||||
# makes the following pattern for setting environment variables
|
||||
# in the current shell work.
|
||||
#
|
||||
# prompt% $(./bootstrap shellinit)
|
||||
#
|
||||
# vim: set ts=2 sts=2 sw=2 et:
|
||||
PROJECT=sprockets
|
||||
|
||||
if test -e /var/run/docker.sock
|
||||
then
|
||||
DOCKER_IP=127.0.0.1
|
||||
else
|
||||
docker-machine status ${PROJECT} >/dev/null 2>/dev/null
|
||||
RESULT=$?
|
||||
if [ ${RESULT} -ne 0 ]
|
||||
then
|
||||
docker-machine create --driver virtualbox ${PROJECT}
|
||||
fi
|
||||
eval $(docker-machine env ${PROJECT} 2>/dev/null) || {
|
||||
echo "Failed to initialize docker environment"
|
||||
exit 2
|
||||
}
|
||||
DOCKER_IP=$(docker-machine ip ${PROJECT})
|
||||
fi
|
||||
|
||||
COMPOSE_ARGS=
|
||||
if test -n "${DOCKER_COMPOSE_PREFIX}"
|
||||
then
|
||||
COMPOSE_ARGS="-p ${DOCKER_COMPOSE_PREFIX}"
|
||||
fi
|
||||
|
||||
get_exposed_port() {
|
||||
docker-compose ${COMPOSE_ARGS} port $1 $2 | cut -d: -f2
|
||||
}
|
||||
|
||||
build_env_file() {
|
||||
DYNAMODB_PORT=$(get_exposed_port dynamodb 7777)
|
||||
(echo "export DOCKER_COMPOSE_PREFIX=${DOCKER_COMPOSE_PREFIX}"
|
||||
echo "export DOCKER_TLS_VERIFY=${DOCKER_TLS_VERIFY}"
|
||||
echo "export DOCKER_HOST=${DOCKER_HOST}"
|
||||
echo "export DOCKER_CERT_PATH=${DOCKER_CERT_PATH}"
|
||||
echo "export DOCKER_MACHINE_NAME=${DOCKER_MACHINE_NAME}"
|
||||
echo "export DYNAMODB_ENDPOINT=http://${DOCKER_IP}:${DYNAMODB_PORT}"
|
||||
) > $1
|
||||
}
|
||||
|
||||
set -e
|
||||
|
||||
mkdir -p build
|
||||
|
||||
if test "$1" = 'shellinit'
|
||||
then
|
||||
# just build the environment file from docker containers
|
||||
build_env_file build/test-environment
|
||||
else
|
||||
docker-compose ${COMPOSE_ARGS} stop
|
||||
docker-compose ${COMPOSE_ARGS} rm --force
|
||||
docker-compose ${COMPOSE_ARGS} up -d
|
||||
build_env_file build/test-environment
|
||||
fi
|
||||
cat build/test-environment
|
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
%YAML 1.2
|
||||
---
|
||||
dynamodb:
|
||||
image: tray/dynamodb-local
|
||||
command: -inMemory -port 7777
|
||||
ports:
|
||||
- 7777
|
|
@ -1,2 +1,5 @@
|
|||
nose>=1.3.7,<2
|
||||
coverage>=3.7,<4
|
||||
arrow>=0.7.0,<1
|
||||
mock>=1.3.0,<2
|
||||
codecov>=1.6.3,<2
|
||||
|
|
|
@ -6,4 +6,11 @@ except ImportError as error:
|
|||
|
||||
version_info = (0, 0, 0)
|
||||
__version__ = '.'.join(str(v) for v in version_info)
|
||||
__all__ = ['DynamoDB', 'version_info', '__version__']
|
||||
|
||||
# Response constants
|
||||
TABLE_ACTIVE = 'ACTIVE'
|
||||
TABLE_CREATING = 'CREATING'
|
||||
TABLE_DELETING = 'DELETING'
|
||||
TABLE_DISABLED = 'DISABLED'
|
||||
TABLE_UPDATING = 'UPDATING'
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
194
sprockets/clients/dynamodb/exceptions.py
Normal file
194
sprockets/clients/dynamodb/exceptions.py
Normal file
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
DynamoDB Exceptions
|
||||
===================
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class DynamoDBException(Exception):
|
||||
"""Base exception that is extended by all exceptions raised by
|
||||
tornado_dynamodb.
|
||||
|
||||
:ivar msg: The error message
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DynamoDBException, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ConditionalCheckFailedException(DynamoDBException):
|
||||
"""A condition specified in the operation could not be evaluated."""
|
||||
pass
|
||||
|
||||
|
||||
class ConfigNotFound(DynamoDBException):
|
||||
"""The configuration file could not be parsed."""
|
||||
pass
|
||||
|
||||
|
||||
class ConfigParserError(DynamoDBException):
|
||||
"""Error raised when parsing a configuration file with
|
||||
:class:`~configparser.RawConfigParser`
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InternalFailure(DynamoDBException):
|
||||
"""The request processing has failed because of an unknown error, exception
|
||||
or failure.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ItemCollectionSizeLimitExceeded(DynamoDBException):
|
||||
"""An item collection is too large. This exception is only returned for
|
||||
tables that have one or more local secondary indexes.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAction(DynamoDBException):
|
||||
"""The action or operation requested is invalid. Verify that the action is
|
||||
typed correctly.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParameterCombination(DynamoDBException):
|
||||
"""Parameters that must not be used together were used together."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParameterValue(DynamoDBException):
|
||||
"""An invalid or out-of-range value was supplied for the input parameter."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQueryParameter(DynamoDBException):
|
||||
"""The AWS query string is malformed or does not adhere to AWS standards."""
|
||||
pass
|
||||
|
||||
|
||||
class LimitExceeded(DynamoDBException):
|
||||
"""The number of concurrent table requests (cumulative number of tables in
|
||||
the ``CREATING``, ``DELETING`` or ``UPDATING`` state) exceeds the maximum
|
||||
allowed of ``10``.
|
||||
|
||||
Also, for tables with secondary indexes, only one of those tables can be in
|
||||
the ``CREATING`` state at any point in time. Do not attempt to create more
|
||||
than one such table simultaneously.
|
||||
|
||||
The total limit of tables in the ``ACTIVE`` state is ``250``.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MalformedQueryString(DynamoDBException):
|
||||
"""The query string contains a syntax error."""
|
||||
pass
|
||||
|
||||
|
||||
class MissingParameter(DynamoDBException):
|
||||
"""A required parameter for the specified action is not supplied."""
|
||||
pass
|
||||
|
||||
|
||||
class NoCredentialsError(DynamoDBException):
|
||||
"""Raised when the credentials could not be located."""
|
||||
pass
|
||||
|
||||
|
||||
class NoProfileError(DynamoDBException):
|
||||
"""Raised when the specified profile could not be located."""
|
||||
pass
|
||||
|
||||
|
||||
class OptInRequired(DynamoDBException):
|
||||
"""The AWS access key ID needs a subscription for the service."""
|
||||
pass
|
||||
|
||||
|
||||
class ThroughputExceeded(DynamoDBException):
|
||||
"""Your request rate is too high. The AWS SDKs for DynamoDB automatically
|
||||
retry requests that receive this exception. Your request is eventually
|
||||
successful, unless your retry queue is too large to finish. Reduce the
|
||||
frequency of requests and use exponential backoff. For more information, go
|
||||
to `Error Retries and Exponential Backoff <http://docs.aws.amazon.com/
|
||||
amazondynamodb/latest/developerguide/ErrorHandling.html#APIRetries>`_ in
|
||||
the Amazon DynamoDB Developer Guide.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestException(DynamoDBException):
|
||||
"""A generic HTTP request exception has occurred when communicating with
|
||||
DynamoDB.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestExpired(DynamoDBException):
|
||||
"""The request reached the service more than 15 minutes after the date
|
||||
stamp on the request or more than 15 minutes after the request expiration
|
||||
date (such as for pre-signed URLs), or the date stamp on the request is
|
||||
more than 15 minutes in the future.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceInUse(DynamoDBException):
|
||||
"""he operation conflicts with the resource's availability. For example,
|
||||
you attempted to recreate an existing table, or tried to delete a table
|
||||
currently in the ``CREATING`` state.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceNotFound(DynamoDBException):
|
||||
"""The operation tried to access a nonexistent table or index. The resource
|
||||
might not be specified correctly, or its status might not be ``ACTIVE``.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUnavailable(DynamoDBException):
|
||||
"""The request has failed due to a temporary failure of the server."""
|
||||
pass
|
||||
|
||||
|
||||
class ThrottlingException(DynamoDBException):
|
||||
"""The request was denied due to request throttling."""
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutException(DynamoDBException):
|
||||
"""The request to DynamoDB timed out."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationException(DynamoDBException):
|
||||
"""The input fails to satisfy the constraints specified by an AWS service.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
MAP = {
|
||||
'com.amazonaws.dynamodb.v20120810#InternalFailure': InternalFailure,
|
||||
'com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceeded':
|
||||
ThroughputExceeded,
|
||||
'com.amazonaws.dynamodb.v20120810#ResourceInUseException': ResourceInUse,
|
||||
'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException':
|
||||
ResourceNotFound,
|
||||
'com.amazon.coral.validate#ValidationException': ValidationException
|
||||
}
|
0
tests.py
0
tests.py
216
tests/api_tests.py
Normal file
216
tests/api_tests.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
import datetime
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
|
||||
from tornado import concurrent
|
||||
from tornado import httpclient
|
||||
from tornado import testing
|
||||
from tornado_aws import exceptions as aws_exceptions
|
||||
|
||||
from sprockets.clients import dynamodb
|
||||
from sprockets.clients.dynamodb import exceptions
|
||||
|
||||
|
||||
class AsyncTestCase(testing.AsyncTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AsyncTestCase, self).setUp()
|
||||
self.client = self.get_client()
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
return os.getenv('DYNAMODB_ENDPOINT')
|
||||
|
||||
@staticmethod
|
||||
def generic_table_definition():
|
||||
return {
|
||||
'TableName': str(uuid.uuid4()),
|
||||
'AttributeDefinitions': [{'AttributeName': 'id',
|
||||
'AttributeType': 'S'}],
|
||||
'KeySchema': [{'AttributeName': 'id', 'KeyType': 'HASH'}],
|
||||
'ProvisionedThroughput': {
|
||||
'ReadCapacityUnits': 5,
|
||||
'WriteCapacityUnits': 5
|
||||
}
|
||||
}
|
||||
|
||||
def get_client(self):
|
||||
return dynamodb.DynamoDB(endpoint=self.endpoint)
|
||||
|
||||
|
||||
class AWSClientTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_config_not_found_exception(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = aws_exceptions.ConfigNotFound(path='/test')
|
||||
with self.assertRaises(exceptions.ConfigNotFound):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_config_parser_error(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = aws_exceptions.ConfigParserError(path='/test')
|
||||
with self.assertRaises(exceptions.ConfigParserError):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_no_credentials_error(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = aws_exceptions.NoCredentialsError()
|
||||
with self.assertRaises(exceptions.NoCredentialsError):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_no_profile_error(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = aws_exceptions.NoProfileError(profile='test-1',
|
||||
path='/test')
|
||||
with self.assertRaises(exceptions.NoProfileError):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_request_exception(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = httpclient.HTTPError(500, 'uh-oh')
|
||||
with self.assertRaises(exceptions.RequestException):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_raises_timeout_exception(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
fetch.side_effect = httpclient.HTTPError(599)
|
||||
with self.assertRaises(exceptions.TimeoutException):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_fetch_future_exception(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
future = concurrent.Future()
|
||||
fetch.return_value = future
|
||||
future.set_exception(exceptions.DynamoDBException())
|
||||
with self.assertRaises(exceptions.DynamoDBException):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
@testing.gen_test
|
||||
def test_empty_fetch_response_raises_dynamodb_exception(self):
|
||||
with mock.patch('tornado_aws.client.AsyncAWSClient.fetch') as fetch:
|
||||
future = concurrent.Future()
|
||||
fetch.return_value = future
|
||||
future.set_result(None)
|
||||
with self.assertRaises(exceptions.DynamoDBException):
|
||||
yield self.client.create_table(self.generic_table_definition())
|
||||
|
||||
|
||||
class CreateTableTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_simple_table(self):
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
self.assertIn(response['TableStatus'],
|
||||
[dynamodb.TABLE_ACTIVE,
|
||||
dynamodb.TABLE_CREATING])
|
||||
|
||||
@testing.gen_test
|
||||
def test_invalid_request(self):
|
||||
definition = {
|
||||
'TableName': str(uuid.uuid4()),
|
||||
'AttributeDefinitions': [{'AttributeName': 'id'}],
|
||||
'KeySchema': [],
|
||||
'ProvisionedThroughput': {
|
||||
'ReadCapacityUnits': 5,
|
||||
'WriteCapacityUnits': 5
|
||||
}
|
||||
}
|
||||
with self.assertRaises(exceptions.ValidationException):
|
||||
yield self.client.create_table(definition)
|
||||
|
||||
@testing.gen_test
|
||||
def test_double_create(self):
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
self.assertIn(response['TableStatus'],
|
||||
[dynamodb.TABLE_ACTIVE,
|
||||
dynamodb.TABLE_CREATING])
|
||||
with self.assertRaises(exceptions.ResourceInUse):
|
||||
response = yield self.client.create_table(definition)
|
||||
|
||||
|
||||
class DeleteTableTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_delete_table(self):
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
yield self.client.delete_table(definition['TableName'])
|
||||
with self.assertRaises(exceptions.ResourceNotFound):
|
||||
yield self.client.describe_table(definition['TableName'])
|
||||
|
||||
@testing.gen_test
|
||||
def test_table_not_found(self):
|
||||
table = str(uuid.uuid4())
|
||||
with self.assertRaises(exceptions.ResourceNotFound):
|
||||
yield self.client.delete_table(table)
|
||||
|
||||
|
||||
class DescribeTableTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_describe_table(self):
|
||||
# Create the table first
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
|
||||
# Describe the table
|
||||
response = yield self.client.describe_table(definition['TableName'])
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
self.assertEqual(response['TableStatus'],
|
||||
dynamodb.TABLE_ACTIVE)
|
||||
|
||||
@testing.gen_test
|
||||
def test_table_not_found(self):
|
||||
table = str(uuid.uuid4())
|
||||
with self.assertRaises(exceptions.ResourceNotFound):
|
||||
yield self.client.describe_table(table)
|
||||
|
||||
|
||||
class ListTableTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_list_tables(self):
|
||||
# Create the table first
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
|
||||
# Describe the table
|
||||
response = yield self.client.list_tables(limit=100)
|
||||
self.assertIn(definition['TableName'], response['TableNames'])
|
||||
|
||||
|
||||
class PutGetDeleteTests(AsyncTestCase):
|
||||
|
||||
@testing.gen_test
|
||||
def test_put_item(self):
|
||||
# Create the table first
|
||||
definition = self.generic_table_definition()
|
||||
response = yield self.client.create_table(definition)
|
||||
self.assertEqual(response['TableName'], definition['TableName'])
|
||||
|
||||
row_id = uuid.uuid4()
|
||||
|
||||
# Describe the table
|
||||
yield self.client.put_item(
|
||||
definition['TableName'],
|
||||
{'id': row_id, 'created_at': datetime.datetime.utcnow()})
|
||||
|
||||
response = yield self.client.get_item(definition['TableName'],
|
||||
{'id': row_id})
|
||||
self.assertEqual(response['id'], row_id)
|
122
tests/utils_tests.py
Normal file
122
tests/utils_tests.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
import datetime
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
import arrow
|
||||
|
||||
from sprockets.clients.dynamodb import utils
|
||||
|
||||
|
||||
class UTC(datetime.tzinfo):
|
||||
def utcoffset(self, dt):
|
||||
return datetime.timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
return 'UTC'
|
||||
|
||||
def dst(self, dt):
|
||||
return datetime.timedelta(0)
|
||||
|
||||
|
||||
class MarshallTests(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def test_complex_document(self):
|
||||
uuid_value = uuid.uuid4()
|
||||
arrow_value = arrow.utcnow()
|
||||
dt_value = datetime.datetime.utcnow().replace(tzinfo=UTC())
|
||||
value = {
|
||||
'key1': 'str',
|
||||
'key2': 10,
|
||||
'key3': {
|
||||
'sub-key1': 20,
|
||||
'sub-key2': True,
|
||||
'sub-key3': 'value'
|
||||
},
|
||||
'key4': None,
|
||||
'key5': ['one', 'two', 'three', 4, None, True],
|
||||
'key6': set(['a', 'b', 'c']),
|
||||
'key7': {1, 2, 3, 4},
|
||||
'key8': arrow_value,
|
||||
'key9': uuid_value,
|
||||
'key10': b'\0x01\0x02\0x03',
|
||||
'key11': {b'\0x01\0x02\0x03', b'\0x04\0x05\0x06'},
|
||||
'key12': dt_value
|
||||
}
|
||||
expectation = {
|
||||
'key1': {'S': 'str'},
|
||||
'key2': {'N': '10'},
|
||||
'key3': {'M':
|
||||
{
|
||||
'sub-key1': {'N': '20'},
|
||||
'sub-key2': {'BOOL': True},
|
||||
'sub-key3': {'S': 'value'}
|
||||
}
|
||||
},
|
||||
'key4': {'NULL': True},
|
||||
'key5': {'L': [{'S': 'one'}, {'S': 'two'}, {'S': 'three'},
|
||||
{'N': '4'}, {'NULL': True}, {'BOOL': True}]},
|
||||
'key6': {'SS': ['a', 'b', 'c']},
|
||||
'key7': {'NS': ['1', '2', '3', '4']},
|
||||
'key8': {'S': arrow_value.isoformat()},
|
||||
'key9': {'S': str(uuid_value)},
|
||||
'key10': {'B': b'\0x01\0x02\0x03'},
|
||||
'key11': {'BS': [b'\0x01\0x02\0x03', b'\0x04\0x05\0x06']},
|
||||
'key12': {'S': dt_value.isoformat()}
|
||||
}
|
||||
self.assertDictEqual(expectation, utils.marshall(value))
|
||||
|
||||
def test_value_error_raised_on_unsupported_type(self):
|
||||
self.assertRaises(ValueError, utils.marshall, {'key': self})
|
||||
|
||||
def test_value_error_raised_on_mixed_set(self):
|
||||
self.assertRaises(ValueError, utils.marshall, {'key': {1, 'two', 3}})
|
||||
|
||||
|
||||
class UnmarshallTests(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def test_complex_document(self):
|
||||
uuid_value = uuid.uuid4()
|
||||
dt_value = arrow.utcnow()
|
||||
value = {
|
||||
'key1': {'S': 'str'},
|
||||
'key2': {'N': '10'},
|
||||
'key3': {'M':
|
||||
{
|
||||
'sub-key1': {'N': '20'},
|
||||
'sub-key2': {'BOOL': True},
|
||||
'sub-key3': {'S': 'value'}
|
||||
}
|
||||
},
|
||||
'key4': {'NULL': True},
|
||||
'key5': {'L': [{'S': 'one'}, {'S': 'two'}, {'S': 'three'},
|
||||
{'N': '4'}, {'NULL': True}, {'BOOL': True}]},
|
||||
'key6': {'SS': ['a', 'b', 'c']},
|
||||
'key7': {'NS': ['1', '2', '3', '4']},
|
||||
'key8': {'S': dt_value.isoformat()},
|
||||
'key9': {'S': str(uuid_value)},
|
||||
'key10': {'B': b'\0x01\0x02\0x03'},
|
||||
'key11': {'BS': [b'\0x01\0x02\0x03', b'\0x04\0x05\0x06']}
|
||||
}
|
||||
expectation = {
|
||||
'key1': 'str',
|
||||
'key2': 10,
|
||||
'key3': {
|
||||
'sub-key1': 20,
|
||||
'sub-key2': True,
|
||||
'sub-key3': 'value'
|
||||
},
|
||||
'key4': None,
|
||||
'key5': ['one', 'two', 'three', 4, None, True],
|
||||
'key6': {'a', 'b', 'c'},
|
||||
'key7': {1, 2, 3, 4},
|
||||
'key8': dt_value.isoformat(),
|
||||
'key9': uuid_value,
|
||||
'key10': b'\0x01\0x02\0x03',
|
||||
'key11': {b'\0x01\0x02\0x03', b'\0x04\0x05\0x06'}
|
||||
}
|
||||
self.assertDictEqual(expectation, utils.unmarshall(value))
|
||||
|
||||
def test_value_error_raised_on_unsupported_type(self):
|
||||
self.assertRaises(ValueError, utils.unmarshall, {'key': {'T': 1}})
|
Loading…
Reference in a new issue