Skip to content

Commit

Permalink
Merge pull request #137 from vintasoftware/pull87
Browse files Browse the repository at this point in the history
Rebases #87
  • Loading branch information
filipeximenes authored May 24, 2017
2 parents e18304c + ebfc846 commit b40e965
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 4 deletions.
26 changes: 24 additions & 2 deletions docs/source/buildingawrapper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ You might want to use one of the following mixins to help you with data format h

- ``FormAdapterMixin``
- ``JSONAdapterMixin``
- ``XMLAdapterMixin``


Exceptions
Expand Down Expand Up @@ -130,6 +131,27 @@ Once these methods are implemented, the client can be instantiated with ```refre
def is_authentication_expired(self, exception, *args, **kwargs):
....
def refresh_authentication(self, api_params, *args, **kwargs):
...
def refresh_authentication(self, api_params, *args, **kwargs):
...
XMLAdapterMixin Configuration (only if required)
------------------------------------------------

Additionally, the XMLAdapterMixin accepts configuration keyword arguments to be passed to the xmltodict library during parsing and unparsing by prefixing the xmltodict keyword with ``xmltodict_parse__`` or ``xmltodict_unparse`` respectively. These parameters should be configured so that the end-user has a consistent experience across multiple Tapioca wrappers irrespective of various API requirements from wrapper to wrapper.

Note that the end-user should **not** need to modify these keyword arguments themselves. See xmltodict `docs <http://xmltodict.readthedocs.org/en/latest/>`_ and `source <https://github.com/martinblech/xmltodict>`_ for valid parameters.

Users should be able to construct dictionaries as defined by the xmltodict library, and responses should be returned in the canonical format.

Example XMLAdapterMixin configuration keywords:

.. code-block:: python
class MyXMLClientAdapter(XMLAdapterMixin, TapiocaAdapter):
...
def get_request_kwargs(self, api_params, *args, **kwargs):
...
# omits XML declaration when constructing requests from dictionary
kwargs['xmltodict_unparse__full_document'] = False
...
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'requests>=2.6',
'arrow>=0.6.0,<0.7',
'six>=1',
'xmltodict>=0.9.2'
]
test_requirements = [
'responses>=0.5',
Expand Down
43 changes: 43 additions & 0 deletions tapioca/adapters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# coding: utf-8

import json
import xmltodict
from collections import Mapping

from .tapioca import TapiocaInstantiator
from .exceptions import (
Expand Down Expand Up @@ -115,3 +117,44 @@ def format_data_to_request(self, data):
def response_to_native(self, response):
if response.content.strip():
return response.json()


class XMLAdapterMixin(object):

def _input_branches_to_xml_bytestring(self, data):
if isinstance(data, Mapping):
return xmltodict.unparse(
data, **self._xmltodict_unparse_kwargs).encode('utf-8')
try:
return data.encode('utf-8')
except Exception as e:
raise type(e)('Format not recognized, please enter an XML as string or a dictionary'
'in xmltodict spec: \n%s' % e.message)

def get_request_kwargs(self, api_params, *args, **kwargs):
# stores kwargs prefixed with 'xmltodict_unparse__' for use by xmltodict.unparse
self._xmltodict_unparse_kwargs = {k[len('xmltodict_unparse__'):]: kwargs.pop(k)
for k in kwargs.copy().keys()
if k.startswith('xmltodict_unparse__')}
# stores kwargs prefixed with 'xmltodict_parse__' for use by xmltodict.parse
self._xmltodict_parse_kwargs = {k[len('xmltodict_parse__'):]: kwargs.pop(k)
for k in kwargs.copy().keys()
if k.startswith('xmltodict_parse__')}

arguments = super(XMLAdapterMixin, self).get_request_kwargs(
api_params, *args, **kwargs)

if 'headers' not in arguments:
arguments['headers'] = {}
arguments['headers']['Content-Type'] = 'application/xml'
return arguments

def format_data_to_request(self, data):
if data:
return self._input_branches_to_xml_bytestring(data)

def response_to_native(self, response):
if response.content.strip():
if 'xml' in response.headers['content-type']:
return xmltodict.parse(response.content, **self._xmltodict_parse_kwargs)
return {'text': response.text}
10 changes: 9 additions & 1 deletion tests/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import unicode_literals

from tapioca.adapters import (
TapiocaAdapter, JSONAdapterMixin,
TapiocaAdapter, JSONAdapterMixin, XMLAdapterMixin,
generate_wrapper_from_adapter)
from tapioca.serializers import SimpleSerializer

Expand Down Expand Up @@ -82,3 +82,11 @@ def refresh_authentication(self, api_params, *args, **kwargs):


FailTokenRefreshClient = generate_wrapper_from_adapter(FailTokenRefreshClientAdapter)


class XMLClientAdapter(XMLAdapterMixin, TapiocaAdapter):
api_root = 'https://api.test.com'
resource_mapping = RESOURCE_MAPPING


XMLClient = generate_wrapper_from_adapter(XMLClientAdapter)
95 changes: 94 additions & 1 deletion tests/test_tapioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import responses
import json

import xmltodict
from collections import OrderedDict
from decimal import Decimal

from tapioca.tapioca import TapiocaClient
from tapioca.exceptions import ClientError

from tests.client import TesterClient, TokenRefreshClient, FailTokenRefreshClient
from tests.client import TesterClient, TokenRefreshClient, XMLClient, FailTokenRefreshClient


class TestTapiocaClient(unittest.TestCase):
Expand Down Expand Up @@ -531,3 +535,92 @@ def request_callback(request):
response = self.wrapper.test().post()

self.assertEqual(response().refresh_data, 'new_token')


class TestXMLRequests(unittest.TestCase):

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

@responses.activate
def test_xml_post_string(self):
responses.add(responses.POST, self.wrapper.test().data,
body='Any response', status=200, content_type='application/json')

data = ('<tag1 attr1="val1">'
'<tag2>text1</tag2>'
'<tag3>text2</tag3>'
'</tag1>')

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

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

self.assertEqual(request_body, data.encode('utf-8'))

@responses.activate
def test_xml_post_dict(self):
responses.add(responses.POST, self.wrapper.test().data,
body='Any response', status=200, content_type='application/json')

data = OrderedDict([
('tag1', OrderedDict([
('@attr1', 'val1'), ('tag2', 'text1'), ('tag3', 'text2')
]))
])

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

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

self.assertEqual(request_body, xmltodict.unparse(data).encode('utf-8'))

@responses.activate
def test_xml_post_dict_passes_unparse_param(self):
responses.add(responses.POST, self.wrapper.test().data,
body='Any response', status=200, content_type='application/json')

data = OrderedDict([
('tag1', OrderedDict([
('@attr1', 'val1'), ('tag2', 'text1'), ('tag3', 'text2')
]))
])

self.wrapper.test().post(data=data, xmltodict_unparse__full_document=False)

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

self.assertEqual(request_body, xmltodict.unparse(
data, full_document=False).encode('utf-8'))

@responses.activate
def test_xml_returns_text_if_response_not_xml(self):
responses.add(responses.POST, self.wrapper.test().data,
body='Any response', status=200, content_type='any content')

data = OrderedDict([
('tag1', OrderedDict([
('@attr1', 'val1'), ('tag2', 'text1'), ('tag3', 'text2')
]))
])

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

self.assertEqual('Any response', response().data['text'])

@responses.activate
def test_xml_post_dict_returns_dict_if_response_xml(self):
xml_body = '<tag1 attr1="val1">text1</tag1>'
responses.add(responses.POST, self.wrapper.test().data,
body=xml_body, status=200,
content_type='application/xml')

data = OrderedDict([
('tag1', OrderedDict([
('@attr1', 'val1'), ('tag2', 'text1'), ('tag3', 'text2')
]))
])

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

self.assertEqual(response().data, xmltodict.parse(xml_body))

0 comments on commit b40e965

Please sign in to comment.