Skip to content

Commit

Permalink
Merge pull request #79 from vintasoftware/serialization
Browse files Browse the repository at this point in the history
Serialization
  • Loading branch information
filipeximenes committed Nov 8, 2015
2 parents e689acb + 99f9f1b commit b6575ec
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 22 deletions.
50 changes: 44 additions & 6 deletions docs/source/serializers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,35 @@ Serializers
===========


Currently, serializers are only capable of deserilizing data but in future releases they will also be capable of serializing.
Serializers classes are capable of performing serialization and deserialization of data.

**Serialization**: is the transformation of data in a native format (in our case Python data types) into a serialized format (eg.: text). For example, this could be transforming a native python ``Datetime`` instance containing a date into a string.

**Deserialization**: is the transformation of a data which is in a serialized format (eg.: text) into a native format. For example, this could be transforming a string containing a date into a native python ``Datetime`` instance.


Usage
=====

Serialization
-------------

Data serialization is done in the background when tapioca is executing the request. It will traverse any data structure passed to the ``data`` param of the request and convert Python data types into serialized types.

.. code-block:: python
>>> reponse = cli.the_resource().post(data={'date': datetime.today()})
in this example, ``datetime.today()`` will be converted into a string formated date just before the request is executed.

Deserialization
---------------

To deserialize data, you need to transform you client into a executor and then call a deserialization method from it. Eg.:

.. code-block:: python
>>> reponse = cli().get()
>>> reponse = cli.the_resource().get()
>>> print(response.created_at())
<TapiocaClientExecutor object
2015-10-25T22:34:51+00:00>
Expand All @@ -23,7 +40,11 @@ To deserialize data, you need to transform you client into a executor and then c
>>> print(type(respose.created_at().to_datetime()))
datetime.datetime
Clients might have the default ``SimpleSerializer``, some custom serializer designed by the wrapper creator, or even no serializer. Either way you can swap it for one of your own. For this, you only need to pass the desired serializer class during the client initialization.
Swapping default serializer
---------------------------

Clients might have the default ``SimpleSerializer``, some custom serializer designed by the wrapper creator, or even no serializer. Either way you can swap it for one of your own, even if you were not the developer of the library. For this, you only need to pass the desired serializer class during the client initialization.

.. code-block:: python
Expand All @@ -34,8 +55,10 @@ Clients might have the default ``SimpleSerializer``, some custom serializer desi
serializer_class=MyCustomSerializer)
SimpleSerializer
================
Built-ins
=========

.. class:: SimpleSerializer

``SimpleSerialzer`` is a very basic and generic serializer. It is included by default in adapters unless explicitly removed. These are the deserialization methods it provides:

Expand All @@ -51,7 +74,22 @@ Converts data to ``Decimal``
Writing a custom serializer
===========================

To write a custom serializer, you just need to extend the ``BaseSerializer`` class and add the methods you want.
To write a custom serializer, you just need to extend the ``BaseSerializer`` class and add the methods you want. But you can also extend from ``SimpleSerializer`` to inherit its functionalities.

Serializing
-----------
To allow serialization of any desired data type, add a method to your serializer named with in the following pattern: ``serialize_ + name_of_your_data_type_in_lower_case``. Eg.:

.. code-block:: python
class MyCustomDataType(object):
message = ''
class MyCustomSerializer(SimpleSerializer):
def serialize_mycustomdatatype(self, data):
return data.message
Deserializing
-------------
Expand Down
10 changes: 9 additions & 1 deletion tapioca/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ def fill_resource_template_url(self, template, params):
return template.format(**params)

def get_request_kwargs(self, api_params, *args, **kwargs):
serialized = self.serialize_data(kwargs.get('data'))

kwargs.update({
'data': self.format_data_to_request(kwargs.get('data')),
'data': self.format_data_to_request(serialized),
})
return kwargs

Expand All @@ -60,6 +62,12 @@ def process_response(self, response):

return data

def serialize_data(self, data):
if self.serializer:
return self.serializer.serialize(data)

return data

def format_data_to_request(self, data):
raise NotImplementedError()

Expand Down
32 changes: 29 additions & 3 deletions tapioca/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,41 @@ def deserialize(self, method_name, value):
return getattr(self, method_name)(value)
raise NotImplementedError("Desserialization method not found")

def serialize_dict(self, data):
serialized = {}

class SimpleSerializerMixin(object):
for key, value in data.items():
serialized[key] = self.serialize(value)

return serialized

def serialize_list(self, data):
serialized = []
for item in data:
serialized.append(self.serialize(item))

return serialized

def serialize(self, data):
data_type = type(data).__name__

serialize_method = ('serialize_' + data_type).lower()
if hasattr(self, serialize_method):
return getattr(self, serialize_method)(data)

return data


class SimpleSerializer(BaseSerializer):

def to_datetime(self, value):
return arrow.get(value).datetime

def to_decimal(self, value):
return Decimal(value)

def serialize_decimal(self, data):
return str(data)

class SimpleSerializer(SimpleSerializerMixin, BaseSerializer):
pass
def serialize_datetime(self, data):
return arrow.get(data).isoformat()
119 changes: 109 additions & 10 deletions tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import unicode_literals

import arrow
import unittest
import responses
from decimal import Decimal
Expand All @@ -25,6 +26,26 @@ def test_serializer_client_adapter_has_serializer(self):
serializer = self.wrapper._api.serializer
self.assertTrue(isinstance(serializer, BaseSerializer))

@responses.activate
def test_executor_dir_returns_serializer_methods(self):
responses.add(responses.GET, self.wrapper.test().data(),
body='{"date": "2014-11-13T14:53:18.694072+00:00"}',
status=200,
content_type='application/json')

response = self.wrapper.test().get()

e_dir = dir(response())

self.assertIn('to_datetime', e_dir)
self.assertIn('to_decimal', e_dir)


class TestDeserialization(unittest.TestCase):

def setUp(self):
self.wrapper = SerializerClient()

@responses.activate
def test_convert_to_decimal(self):
responses.add(responses.GET, self.wrapper.test().data(),
Expand Down Expand Up @@ -75,16 +96,94 @@ def test_call_conversion_with_no_serializer(self):
with self.assertRaises(NotImplementedError):
response.any_data().to_datetime()

@responses.activate
def test_executor_dir_returns_serializer_methods(self):
responses.add(responses.GET, self.wrapper.test().data(),
body='{"date": "2014-11-13T14:53:18.694072+00:00"}',
status=200,
content_type='application/json')

response = self.wrapper.test().get()
class TestSerialization(unittest.TestCase):

e_dir = dir(response())
def setUp(self):
self.serializer = SimpleSerializer()

self.assertIn('to_datetime', e_dir)
self.assertIn('to_decimal', e_dir)
def test_serialize_int(self):
data = 1

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialize_str(self):
data = 'the str'

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialize_float(self):
data = 1.23

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialize_none(self):
data = None

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialization_of_simple_dict(self):
data = {
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
}

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialization_of_simple_list(self):
data = [1, 2, 3, 4, 5]

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_serialization_of_nested_list_in_dict(self):
data = {
'key1': [1, 2, 3, 4, 5],
'key2': [1],
'key3': [1, 2, 5],
}

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_multi_level_serializations(self):
data = [
{'key1': [1, 2, 3, 4, 5]},
{'key2': [1]},
{'key3': [1, 2, 5]},
]

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, data)

def test_decimal_serialization(self):
data = {
'key': [Decimal('1.0'), Decimal('1.1'), Decimal('1.2')]
}

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, {'key': ['1.0', '1.1', '1.2']})

def test_datetime_serialization(self):
string_date = '2014-11-13T14:53:18.694072+00:00'

data = [arrow.get(string_date).datetime]

serialized = self.serializer.serialize(data)

self.assertEqual(serialized, [string_date])
31 changes: 29 additions & 2 deletions tests/test_tapioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
from __future__ import unicode_literals

import unittest

import responses
import arrow
import json
from decimal import Decimal

from tapioca.tapioca import TapiocaClient
from tapioca.serializers import SimpleSerializer

from tests.client import TesterClient
from tests.client import TesterClient, SerializerClient


class TestTapiocaClient(unittest.TestCase):
Expand Down Expand Up @@ -393,3 +396,27 @@ def test_simple_pages_max_item_zero_iterator(self):
for item in response().pages(max_items=0):
self.assertIn(item.key().data(), 'value')
iterations_count += 1

@responses.activate
def test_data_serialization(self):
wrapper = SerializerClient()

responses.add(responses.POST, self.wrapper.test().data(),
body='{}', status=200, content_type='application/json')

string_date = '2014-11-13T14:53:18.694072+00:00'
string_decimal = '1.45'

data = {
'date': arrow.get(string_date).datetime,
'decimal': Decimal(string_decimal),
}

wrapper.test().post(data=data)

request_body = responses.calls[0].request.body

self.assertEqual(
json.loads(request_body),
{'date': string_date, 'decimal': string_decimal})

0 comments on commit b6575ec

Please sign in to comment.