diff --git a/.travis.yml b/.travis.yml index 7f1c93c..c1db3f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python python: + - "3.5" - "3.4" - "3.3" diff --git a/requirements.txt b/requirements.txt index 2a12b39..d326324 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ boto==2.35.2 +oto==1.0.1 py==1.4.31 pytest==3.0.2 pytest-cov==2.3.1 diff --git a/setup.py b/setup.py index fa14868..a5905c8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='Sukimu', - version='0.0.7', + version='1.0.1', url='https://github.com/xethorn/sukimu', author='Michael Ortali', author_email='github@xethorn.net', @@ -17,4 +17,5 @@ classifiers=[ 'Development Status :: Alpha', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', ],) diff --git a/sukimu/consts.py b/sukimu/consts.py index b9b5e52..16cc93c 100644 --- a/sukimu/consts.py +++ b/sukimu/consts.py @@ -8,3 +8,6 @@ SORT_ASCENDING = 2 SORT_DESCENDING = 1 + +ERROR_CODE_VALIDATION = 'validation_errors' +ERROR_CODE_DUPLICATE_KEY = 'duplicate_key' diff --git a/sukimu/dynamodb.py b/sukimu/dynamodb.py index 197c82d..c8ab49c 100644 --- a/sukimu/dynamodb.py +++ b/sukimu/dynamodb.py @@ -2,10 +2,11 @@ """ from boto.dynamodb2 import table +from oto import response +from oto import status from sukimu import consts from sukimu import operations -from sukimu import response from sukimu import schema @@ -72,7 +73,7 @@ def update(self, item, data): Response: The response of the update. """ - if not item.success: + if not item: return item item = item.message @@ -81,7 +82,7 @@ def update(self, item, data): save = item.partial_save() return response.Response( - status=response.Status.OK if save else response.Status.ERROR, + status=status.OK if save else status.ACCEPTED, message=item) def delete(self, item): @@ -95,8 +96,7 @@ def delete(self, item): deleted = item.delete() return response.Response( - status=response.Status.OK if deleted else response.Status.ERROR, - message=None) + status=status.OK if deleted else status.BAD_REQUEST) def fetch(self, query, sort=None, limit=None, index=None): """Fetch one or more entries. @@ -153,11 +153,10 @@ def fetch(self, query, sort=None, limit=None, index=None): if not len(dynamo): return response.Response( - status=response.Status.NOT_FOUND, + status=status.NOT_FOUND, message=[]) return response.Response( - status=response.Status.OK, message=[obj for obj in dynamo]) def fetch_many(self, key, values, index=None): @@ -179,13 +178,11 @@ def fetch_many(self, key, values, index=None): self.fetch_one( index=index, **{key: operations.Equal(value)}).message) - status = response.Status.OK + data_status = status.OK if not message: - status = response.Status.NOT_FOUND + data_status = status.NOT_FOUND - return response.Response( - status=response.Status.OK, - message=message) + return response.Response(status=data_status, message=message) def fetch_one(self, index=None, **query): """Get one item. @@ -200,9 +197,7 @@ def fetch_one(self, index=None, **query): if not found, the status is set to NOT_FOUND. """ - default_response = response.Response( - status=response.Status.NOT_FOUND, - message=None) + default_response = response.Response(status=status.NOT_FOUND) field_names = list(query.keys()) required = 1 @@ -231,9 +226,7 @@ def fetch_one(self, index=None, **query): item = data[0] if item: - return response.Response( - status=response.Status.OK, - message=item) + return response.Response(message=item) return default_response diff --git a/sukimu/response.py b/sukimu/response.py deleted file mode 100644 index 7acde1f..0000000 --- a/sukimu/response.py +++ /dev/null @@ -1,104 +0,0 @@ -from sukimu import exceptions - - -class Status(): - ERROR = 500 - INVALID_FIELDS = 501 - FIELD_VALUE_ALREADY_USED = 502 - KEY_VALUE_ALREADY_USED = 503 - NOT_FOUND = 400 - OK = 200 - - -class Response(): - """Response - - The Response class provides an envelop for the messages that are going from - the Schema and the Table. They ensure the data is in good standing (and if - not, they provide context around the errors.) - - """ - - def __init__(self, status, message, errors=None): - """Response constructor. - - See: - `create_error_response` and `create_success_response` for - shortcuts. - - Args: - status (int): The status of the Response. - message: The message of the response. The message can contain any - datastructures (dictionary, set, list) - errors (list): The errors (if some have occured.) - """ - - self.status = status - self.errors = errors or {} - self.message = message or {} - self.formatted_message = None - - def __getattr__(self, key): - """Shortcut to access properties on the response message. - - If the response message (and only if the response message is a - dictionary), this method provides a shortcut to access its properties. - - Raises: - FieldException: If the response format is not a dictionary. - - Args: - key (string): The key to get. - - Return: - The value for this specific key (and if it does not exist the - default value.) - """ - - if not self.formatted_message and isinstance(self.message, dict): - self.formatted_message = self.message - elif not self.formatted_message: - try: - self.formatted_message = dict(self.message) - except: - pass - - if self.formatted_message: - return self.formatted_message.get(key) - - raise exceptions.RESPONSE_FORMAT_ATTRIBUTES_UNAVAILABLE - - @property - def success(self): - return self.status == Status.OK - - -def create_error_response(errors=None, message=None): - """Create an error response. - - Args: - errors (dict): List of errors (if any.) - message: Message that will be provided as part of the response - (optional.) - Return: - `Response`: The error response. - """ - - return Response( - status=Status.ERROR, - errors=errors, - message=message) - - -def create_success_response(message=None): - """Create a success response. - - Args: - message: The response from the schema (optional.) - Return: - `Response`: a successful response. - """ - - return Response( - status=Status.OK, - message=message) diff --git a/sukimu/schema.py b/sukimu/schema.py index bafc25c..360cc69 100644 --- a/sukimu/schema.py +++ b/sukimu/schema.py @@ -80,9 +80,12 @@ def profile(obj): from copy import deepcopy from threading import Thread +from oto import response +from oto import status + +from sukimu import consts from sukimu import exceptions from sukimu import operations -from sukimu import response from sukimu import utils @@ -123,7 +126,7 @@ def validate(self, values, operation): items = set(values.keys()) if operation is operations.READ and not values: - return response.create_success_response() + return response.Response() if operation is operations.CREATE: items = set(self.fields.keys()) @@ -147,14 +150,11 @@ def validate(self, values, operation): errors[name] = e status = False - status = response.Status.OK if errors: - status = response.Status.INVALID_FIELDS + return response.create_error_response( + consts.ERROR_CODE_VALIDATION, errors) - return response.Response( - status=status, - message=data, - errors=errors) + return response.Response(message=data) def ensure_indexes(self, validation_response, current=None): """Ensure index unicity. @@ -176,7 +176,7 @@ def ensure_indexes(self, validation_response, current=None): Response: The response """ - if not validation_response.success: + if not validation_response: return validation_response data = validation_response.message @@ -202,19 +202,16 @@ def ensure_indexes(self, validation_response, current=None): continue ancestor = self.fetch_one(**query) - if ancestor.success: + if ancestor: if not current or dict(ancestor.message) != dict(current): errors.update({ key: exceptions.FIELD_ALREADY_USED for key in keys}) - status = response.Status.OK if errors: - status = response.Status.FIELD_VALUE_ALREADY_USED + return response.create_error_response( + consts.ERROR_CODE_DUPLICATE_KEY, errors) - return response.Response( - message=None, - status=status, - errors=errors) + return response.Response() def generated(self, **dependencies): """Register a generated field. @@ -263,12 +260,12 @@ def fetch(self, fields=None, limit=None, sort=None, index=None, """ validation_response = self.validate(query, operation=operations.READ) - if not validation_response.success: + if not validation_response: return validation_response schema_response = self.table.fetch( query, sort=sort, limit=limit, index=index) - if schema_response.success and fields: + if schema_response and fields: self.decorate_response(schema_response, fields, context=context) return schema_response @@ -287,11 +284,11 @@ def fetch_one(self, fields=None, context=None, **query): validation_response = self.validate(query, operation=operations.READ) - if not validation_response.success: + if not validation_response: return validation_response schema_response = self.table.fetch_one(**query) - if schema_response.success and fields: + if schema_response and fields: self.decorate_response(schema_response, fields, context=context) return schema_response @@ -381,17 +378,15 @@ def create(self, **data): """ validation = self.validate(data, operation=operations.CREATE) - if not validation.success: + if not validation: return validation check = self.ensure_indexes(validation) - if not check.success: + if not check: return check data = self.table.create(validation.message) - return response.Response( - message=data, - status=response.Status.OK) + return response.Response(message=data) def update(self, source, **data): """Update the model from the data passed. @@ -403,21 +398,20 @@ def update(self, source, **data): data = utils.key_exclude(data, source.keys()) data = self.validate(data, operation=operations.READ) - if not data.success: + if not data: return data # Recreate the object - check ancestors. current = self.fetch_one(**{ key: operations.Equal(val) for key, val in source.items()}) - if not current.success: + if not current: return current fields = response.Response( - message=dict(list(source.items()) + list(data.message.items())), - status=response.Status.OK) + message=dict(list(source.items()) + list(data.message.items()))) ancestors = self.ensure_indexes(fields, current.message) - if not ancestors.success: + if not ancestors: return ancestors return self.table.update(current, fields.message) @@ -427,7 +421,7 @@ def delete(self, **source): """ item = self.fetch_one(**source) - if not item.success: + if not item: return item return self.table.delete(item.message) diff --git a/tests/test_dynamodb.py b/tests/test_dynamodb.py index f5c7b41..5206ce1 100644 --- a/tests/test_dynamodb.py +++ b/tests/test_dynamodb.py @@ -4,9 +4,11 @@ from random import random from random import shuffle +from oto import response +from oto import status + from sukimu import consts from sukimu import exceptions -from sukimu import response from sukimu.dynamodb import IndexDynamo from sukimu.dynamodb import IndexDynamo from sukimu.dynamodb import TableDynamo @@ -94,13 +96,14 @@ def test_can_create_fixtures(user_schema, thread_schema): def test_create_an_entry_with_wrong_field(user_schema): resp = user_schema.create(id='30', username='michael', random_field='test') - assert not resp.success + assert not resp assert isinstance( - resp.errors.get('random_field'), exceptions.FieldException) + resp.errors.get('message').get('random_field'), + exceptions.FieldException) resp = user_schema.fetch_one(id=Equal('30')) - assert not resp.success - assert resp.status is response.Status.NOT_FOUND + assert not resp + assert resp.status is status.NOT_FOUND def test_extension(user_schema): @@ -115,15 +118,15 @@ def test_extension(user_schema): def test_create_an_entry_for_user(user_schema): resp = user_schema.create(id='30', username='michael') - assert resp.success + assert resp def test_update_an_entry_for_user(user_schema): resp = user_schema.create(id='30', username='michael') - assert resp.success + assert resp resp = user_schema.update(dict(id='30'), username='joe') - assert resp.success + assert resp def test_delete_an_entry_for_user(user_schema): @@ -131,10 +134,10 @@ def test_delete_an_entry_for_user(user_schema): user_schema.create(id='40', username='michael2') resp = user_schema.delete(id=Equal('30')) - assert resp.success + assert resp resp = user_schema.fetch_one(id=Equal(30)) - assert not resp.success + assert not resp def test_update_an_entry_on_existing_key(user_schema): @@ -142,22 +145,24 @@ def test_update_an_entry_on_existing_key(user_schema): user_schema.create(id='30', username='joe') resp = user_schema.update(dict(id='30'), username='michael') - assert not resp.success - assert isinstance(resp.errors.get('username'), exceptions.FieldException) + assert not resp + assert isinstance( + resp.errors.get('message').get('username'), exceptions.FieldException) resp = user_schema.fetch_one(id=Equal('30')) - assert resp.username == 'joe' + assert resp.message.get('username') == 'joe' def test_create_an_entry_on_existing_user_id(user_schema): resp = user_schema.create(id='30', username='michael') - assert resp.success + assert resp resp = user_schema.create(id='30', username='otherusername') - assert not resp.success - assert not resp.errors.get('username') - assert resp.status is response.Status.FIELD_VALUE_ALREADY_USED - assert isinstance(resp.errors.get('id'), exceptions.FieldException) + assert not resp + assert not resp.errors.get('message').get('username') + assert resp.errors.get('code') is consts.ERROR_CODE_DUPLICATE_KEY + assert isinstance( + resp.errors.get('message').get('id'), exceptions.FieldException) def test_create_an_entry_with_map_data(user_schema): @@ -168,70 +173,72 @@ def test_create_an_entry_with_map_data(user_schema): key1='value', key2=dict( key1='value'))) - assert resp.success + assert resp resp = user_schema.fetch_one(id=Equal('190')) - assert resp.success + assert resp assert isinstance(resp.message.get('map_field'), dict) def test_create_an_entry_on_existing_user_username(user_schema): resp = user_schema.create(id='30', username='michael') - assert resp.success + assert resp resp = user_schema.create(id='20', username='michael') - assert not resp.success - assert not resp.errors.get('id') - assert resp.status is response.Status.FIELD_VALUE_ALREADY_USED - assert isinstance(resp.errors.get('username'), exceptions.FieldException) + assert not resp + assert not resp.errors.get('message').get('id') + assert resp.errors.get('code') is consts.ERROR_CODE_DUPLICATE_KEY + assert isinstance( + resp.errors.get('message').get('username'), exceptions.FieldException) def test_create_an_entry_on_existing_user_username_and_id(user_schema): resp = user_schema.create(id='30', username='michael') - assert resp.success + assert resp resp = user_schema.create(id='20', username='michael') - assert not resp.success - assert not resp.errors.get('id') - assert resp.status is response.Status.FIELD_VALUE_ALREADY_USED - assert isinstance(resp.errors.get('username'), exceptions.FieldException) + assert not resp + assert not resp.errors.get('message').get('id') + assert resp.errors.get('code') is consts.ERROR_CODE_DUPLICATE_KEY + assert isinstance( + resp.errors.get('message').get('username'), exceptions.FieldException) def test_thread_creation(thread_schema): resp = thread_schema.create( forum_name='News', thread_title='title', thread_author='user', thread_content='content') - assert resp.success + assert resp resp = thread_schema.fetch_one( forum_name=Equal('News'), thread_title=Equal('title')) - assert resp.success - assert resp.thread_author == 'user' + assert resp + assert resp.message.get('thread_author') == 'user' resp = thread_schema.fetch_one( forum_name=Equal('News'), thread_author=Equal('user')) - assert resp.success - assert resp.thread_title == 'title' + assert resp + assert resp.message.get('thread_title') == 'title' resp = thread_schema.fetch_one( thread_title=Equal('title'), thread_author=Equal('user')) - assert resp.success - assert resp.forum_name == 'News' + assert resp + assert resp.message.get('forum_name') == 'News' resp = thread_schema.create( forum_name='Updates', thread_title='Title2', thread_author='user', thread_content='content') - assert resp.success + assert resp resp = thread_schema.create( forum_name='Updates', thread_title='Title3', thread_author='user2', thread_content='content') - assert resp.success + assert resp resp = thread_schema.create( forum_name='Others', thread_title='Title', thread_author='user4', thread_content='foobar') - assert resp.success + assert resp def test_thread_creation_on_duplicate_indexes(thread_schema): @@ -242,28 +249,28 @@ def test_thread_creation_on_duplicate_indexes(thread_schema): resp = thread_schema.create( forum_name='News', thread_title='title', thread_author='user', thread_content='content') - assert resp.success + assert resp resp = thread_schema.create( forum_name='News', thread_title='title', thread_author='user2', thread_content='content') - assert not resp.success - assert resp.errors.get('forum_name') - assert resp.errors.get('thread_title') + assert not resp + assert resp.errors.get('message').get('forum_name') + assert resp.errors.get('message').get('thread_title') resp = thread_schema.create( forum_name='News', thread_title='title2', thread_author='user', thread_content='content') - assert not resp.success - assert resp.errors.get('thread_author') - assert resp.errors.get('forum_name') + assert not resp + assert resp.errors.get('message').get('thread_author') + assert resp.errors.get('message').get('forum_name') resp = thread_schema.create( forum_name='Other', thread_title='title', thread_author='user', thread_content='content') - assert not resp.success - assert resp.errors.get('thread_title') - assert resp.errors.get('thread_author') + assert not resp + assert resp.errors.get('message').get('thread_title') + assert resp.errors.get('message').get('thread_author') def test_create_dynamo_schema(table_name): @@ -281,16 +288,16 @@ def test_fetch_on_index(thread_schema): resp = thread_schema.create( forum_name='News', thread_title='title', thread_author='user', thread_content='content') - assert resp.success + assert resp resp = thread_schema.fetch( forum_name=Equal('News'), thread_title=Equal('title')) - assert resp.success + assert resp assert resp.message[0].get('thread_author') == 'user' resp = thread_schema.fetch( thread_title=Equal('title'), thread_author=Equal('user')) - assert resp.success + assert resp assert resp.message[0].get('forum_name') == 'News' @@ -298,7 +305,7 @@ def test_fetch_many(user_schema): user_schema.create(id='30', username='michael1') user_schema.create(id='40', username='michael2') resp = user_schema.fetch(username=In('michael1', 'michael2')) - assert resp.success + assert resp assert len(resp.message) == 2 @@ -412,18 +419,18 @@ def history(item, fields): return {'length': 20} response = user_schema.create(id='testextension', username='michael') - assert response.success + assert response response = user_schema.fetch_one( username=Equal('michael'), fields=['stats.foobar', 'stats.tests.bar']) - assert response.stats.get('days') == 10 - assert 'foobar' in response.stats.get('fields') - assert 'tests.bar' in response.stats.get('fields') + assert response.message.get('stats').get('days') == 10 + assert 'foobar' in response.message.get('stats').get('fields') + assert 'tests.bar' in response.message.get('stats').get('fields') response = user_schema.fetch_one( username=Equal('michael'), fields=['history', 'stats.foobar', 'stats.tests.bar']) - assert response.stats.get('days') == 10 - assert 'foobar' in response.stats.get('fields') - assert 'tests.bar' in response.stats.get('fields') - assert response.history.get('length') == 20 + assert response.message.get('stats').get('days') == 10 + assert 'foobar' in response.message.get('stats').get('fields') + assert 'tests.bar' in response.message.get('stats').get('fields') + assert response.message.get('history').get('length') == 20 diff --git a/tests/test_response.py b/tests/test_response.py deleted file mode 100644 index 784ca63..0000000 --- a/tests/test_response.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest - -from sukimu import response -from sukimu import exceptions - - -def test_response_creation(): - message = 'Hello World' - resp = response.Response(response.Status.OK, message) - assert resp.status is response.Status.OK - assert resp.message is message - assert not resp.errors - - -def test_response_errors(): - """Response errors should always be a dictionary. - """ - - message = '' - resp = response.Response(response.Status.OK, message, errors=None) - assert isinstance(resp.errors, dict) - - -def test_get_quick_access_to_message(): - """Only works if the message is a dictionary. - """ - - message = dict(attribute='value') - resp = response.create_success_response(message=message) - assert resp.attribute == 'value' - - -def test_access_to_non_dict_message(): - """Only works if the message is a dictionary. - """ - - message = ('attribute', 'value') - resp = response.create_success_response(message=message) - with pytest.raises(exceptions.FieldException): - assert resp.attribute == 'value' - - -def test_response_success(): - resp = response.Response(response.Status.OK, '') - assert resp.success - - resp = response.Response(response.Status.NOT_FOUND, '') - assert not resp.success - - -def test_create_success_response(): - resp = response.create_success_response() - assert resp.success - assert resp.status is response.Status.OK - - -def test_create_error_response(): - resp = response.create_error_response() - assert not resp.success - assert resp.status is response.Status.ERROR diff --git a/tests/test_schema.py b/tests/test_schema.py index 249529e..f1d3311 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,9 +1,11 @@ from unittest import mock import pytest +from oto import response +from oto import status + from sukimu import fields from sukimu import operations -from sukimu import response from sukimu import schema @@ -49,15 +51,15 @@ def test_field_validation_on_create(): s.validate({}) resp = s.validate({'id': 'test'}, operations.CREATE) - assert resp.status is response.Status.INVALID_FIELDS - assert resp.errors.get('username') - assert not resp.errors.get('id') + assert resp.status is status.BAD_REQUEST + assert resp.errors.get('message').get('username') + assert not resp.errors.get('message').get('id') resp = s.validate({'username': 'test'}, operations.CREATE) - assert resp.status is response.Status.OK + assert resp.status is status.OK resp = s.validate({}, operations.READ) - assert resp.status is response.Status.OK + assert resp.status is status.OK def test_field_validation_on_read(): @@ -67,7 +69,7 @@ def test_field_validation_on_read(): resp = s.validate( {'username': 'foo', 'unknownfield': 'value'}, operations.READ) - assert resp.status is response.Status.OK + assert resp.status is status.OK assert not resp.message.get('unknownfield') # Fields to validate should be a dictionary of format: @@ -92,22 +94,22 @@ def test_ensure_index(monkeypatch, full_schema): with pytest.raises(Exception): full_schema.ensure_indexes(object()) - error_response = response.create_error_response() + error_response = response.Response(status=status.BAD_REQUEST) assert full_schema.ensure_indexes(error_response) is error_response data = dict(id='id-value', username='username-value') fetch_one = mock.MagicMock(return_value=error_response) - success_response = response.create_success_response(data) + success_response = response.Response(data) monkeypatch.setattr(full_schema, 'fetch_one', fetch_one) resp = full_schema.ensure_indexes(success_response) - assert resp.success + assert resp fetch_one.return_value = success_response resp = full_schema.ensure_indexes(success_response) - assert not resp.success - assert 'id' in resp.errors - assert 'username' in resp.errors + assert not resp + assert 'id' in resp.errors.get('message') + assert 'username' in resp.errors.get('message') def test_extensions(full_schema):