From f8c8581d5794a766c7b16b203400f7da8639acd6 Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Sat, 21 Nov 2015 22:53:31 -0500 Subject: [PATCH 1/6] initial commit with xml and an oauth2 token request helper --- tapioca/adapters.py | 32 +++++ tapioca/oauth2_token_requester.py | 50 +++++++ tapioca/xml_helpers.py | 96 ++++++++++++++ tests/test_xml_helpers.py | 214 ++++++++++++++++++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 tapioca/oauth2_token_requester.py create mode 100644 tapioca/xml_helpers.py create mode 100644 tests/test_xml_helpers.py diff --git a/tapioca/adapters.py b/tapioca/adapters.py index 392df76..167db2a 100644 --- a/tapioca/adapters.py +++ b/tapioca/adapters.py @@ -6,6 +6,8 @@ from .exceptions import ( ResponseProcessException, ClientError, ServerError) from .serializers import SimpleSerializer +from .xml_helpers import ( + etree_elt_dict_to_xml, flat_dict_to_etree_elt_dict, xml_string_to_etree_elt_dict) def generate_wrapper_from_adapter(adapter_class): @@ -109,3 +111,33 @@ def format_data_to_request(self, data): def response_to_native(self, response): if response.content.strip(): return response.json() + + +class XmlAdapterMixin(object): + + def get_request_kwargs(self, api_params, *args, **kwargs): + arguments = super(XmlAdapterMixin, self).get_request_kwargs( + api_params, *args, **kwargs) + + if 'headers' not in arguments: + # allows user to override for formats like 'application/atom+xml' + arguments['headers'] = {} + arguments['headers']['Content-Type'] = 'application/xml' + return arguments + + def format_data_to_request(self, data): + if data: + return etree_elt_dict_to_xml(data) + + def response_to_native(self, response): + if response.content.strip(): + if 'xml' in response.headers['content-type']: + return xml_string_to_etree_elt_dict(response.content) + return {'text': response.text} + + +class FlatXmlAdapterMixin(XmlAdapterMixin): + + def format_data_to_request(self, data): + if data: + return etree_elt_dict_to_xml(flat_dict_to_etree_elt_dict(data)) diff --git a/tapioca/oauth2_token_requester.py b/tapioca/oauth2_token_requester.py new file mode 100644 index 0000000..0335f78 --- /dev/null +++ b/tapioca/oauth2_token_requester.py @@ -0,0 +1,50 @@ +from requests_oauthlib import OAuth2Session + + +class Oauth2TokenRequester(object): + + def __init__(self, + client_id, + client_secret, + redirect_uri, + authorization_base_url, + obtain_token_url, + scope_list, + **auth_base_url_kwargs): + + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.authorization_base_url = authorization_base_url + self.obtain_token_url = obtain_token_url + self.scope_list = scope_list + self.auth_base_url_kwargs = auth_base_url_kwargs + + def request_token(self): + ''' + Uses requests_oauthlib to request a token and response. + Requires your app to have a redirect_uri set up. + + Prompts user through steps of obtaining a token. + + Usage: + o = OauthRequester(**kwargs) # '**kwargs' for brevity. + token = o.request_token() + ''' + oauth = OAuth2Session(self.client_id, + scope=self.scope_list, + redirect_uri=self.redirect_uri) + authorization_url, state = oauth.authorization_url(self.authorization_base_url, + **self.auth_base_url_kwargs) + + print ('\n###### OauthRequester User Prompt ######\n' + '1. Please go to the following URL to authorize access: \n\n%s' % authorization_url) + + redirect_response = raw_input('\n2. Enter the full callback URL that your request was ' + 'redirected to:\n') + + oauth.fetch_token(self.obtain_token_url, + authorization_response=redirect_response, + client_secret=self.client_secret) + + return oauth.token diff --git a/tapioca/xml_helpers.py b/tapioca/xml_helpers.py new file mode 100644 index 0000000..db1e82f --- /dev/null +++ b/tapioca/xml_helpers.py @@ -0,0 +1,96 @@ +from xml.etree import ElementTree +from collections import Mapping +from operator import methodcaller + + +def flat_dict_to_etree_elt_dict(dict_to_convert, _depth=0): + ''' + Convert a special flat format to a dictionary that can be readily mapped to etree.Element. + + The special flat format is intended to be similar to XML arguments and easy to type while + reading XML related API documents. + + This format does not allow for combinations of text, subelements, and tails. It supports + either text or subelements. If you need more flexibility, use the more general etree_elt_dict + format with the etree_elt_dict_to_xml() method. + + A single root node is required. + Double quotes in attribute values are stripped and optional. + + Example Input: + {'root|attr1="val 1"': {'subroot1|attr1="sub val 1"': 'sub text 1', + 'subroot2|attr2="sub val 2"': 'sub text 2'}} + ''' + if not _depth and len(dict_to_convert) > 1: + raise Exception('Multiple root nodes detected, please check input has only one root node.') + node_list = [] + for k, v in dict_to_convert.items(): + etree_elt_dict = {} + etree_elt_dict['tag'], attrib_list = [k.split('|')[0], k.split('|')[1:]] + etree_elt_dict['attrib'] = {k: v.replace('"', '') for k, v + in map(methodcaller('split', '='), attrib_list)} + + # no support for text and subelements for any single node. + if isinstance(v, Mapping): + etree_elt_dict['text'] = '' + etree_elt_dict['sub_elts'] = flat_dict_to_etree_elt_dict(dict_to_convert=v, + _depth=_depth + 1) + elif isinstance(v, str): + etree_elt_dict['text'] = v + etree_elt_dict['sub_elts'] = '' + else: + raise Exception('Child element not of type Mapping or String') + + etree_elt_dict['tail'] = '' + node_list.append(etree_elt_dict) + return node_list if _depth else node_list[0] + + +def _etree_elt_list_to_xml(etree_elt_list): + ''' + Helper method to handle lists of etree_elt_dicts. + ''' + return_list = [] + # todo: test for required keys + for etree_elt_dict in etree_elt_list: + if not isinstance(etree_elt_dict, Mapping): + raise Exception('Structure must be a Mapping object') + node = ElementTree.Element(etree_elt_dict['tag'], + attrib=etree_elt_dict['attrib']) + node.text = etree_elt_dict['text'] + node.tail = etree_elt_dict['tail'] + if etree_elt_dict['sub_elts']: + node.extend(_etree_elt_list_to_xml(etree_elt_dict['sub_elts'])) + return_list.append(node) + return return_list + + +def etree_elt_dict_to_xml(etree_elt_dict): + ''' + Converts an etree_elt_dict into XML. There must be a single root node. + An etree_elt_dict is designed to readily map onto ElementTree.Element. + ''' + if not isinstance(etree_elt_dict, Mapping): + raise Exception('Structure must be a Mapping object') + return bytes( + ElementTree.tostring(_etree_elt_list_to_xml([etree_elt_dict])[0]) + ) + + +def etree_node_to_etree_elt_dict(etree_node): + etree_elt_dict = {} + etree_elt_dict['tag'] = etree_node.tag + etree_elt_dict['attrib'] = etree_node.attrib + etree_elt_dict['text'] = etree_node.text + etree_elt_dict['tail'] = etree_node.tail + + sub_elts = [] + for child_node in etree_node: + sub_elts.append(etree_node_to_etree_elt_dict(child_node)) + etree_elt_dict['sub_elts'] = sub_elts + + return etree_elt_dict + + +def xml_string_to_etree_elt_dict(xml_string): + return etree_node_to_etree_elt_dict(ElementTree.fromstring(xml_string)) diff --git a/tests/test_xml_helpers.py b/tests/test_xml_helpers.py new file mode 100644 index 0000000..fec58ce --- /dev/null +++ b/tests/test_xml_helpers.py @@ -0,0 +1,214 @@ +# coding: utf-8 + +import unittest + +from tapioca.xml_helpers import flat_dict_to_etree_elt_dict +from tapioca.xml_helpers import etree_elt_dict_to_xml + + +# Use sets of 3 to test the translation methods +# Note: There are limitations to the 'flat' format when compared to ElementTree.Element, so these +# tests don't yet cover the full range of inputs to etree_elt_dict_to_xml() +FLAT_1 = {'root1|attr1="my attr value 1"|attr2="my attr value 2"': 'root text'} +ETREE_DICT_1 = {'tag': 'root1', + 'attrib': {'attr1': 'my attr value 1', + 'attr2': 'my attr value 2'}, + 'text': 'root text', + 'tail': '', + 'sub_elts': ''} +XML_STR_1 = b'root text' + +FLAT_2 = {'root2|attr1="my attr value 1"': { + 'subroot1|subattr1="sub attr value 1"': 'subtext 1'}} +ETREE_DICT_2 = {'tag': 'root2', + 'attrib': {'attr1': 'my attr value 1'}, + 'text': '', + 'tail': '', + 'sub_elts': [{'tag': 'subroot1', + 'attrib': {'subattr1': 'sub attr value 1'}, + 'text': 'subtext 1', + 'tail': '', + 'sub_elts': ''}] + } +XML_STR_2 = (b'' + b'subtext 1') + +FLAT_3 = {'root2|attr1="my attr value 1"': { + 'subroot1|subattr1="sub attr value 1"': { + 'subroot2|subattr2="sub attr value 2"': 'subtext 2'} + }} +ETREE_DICT_3 = {'tag': 'root2', + 'attrib': {'attr1': 'my attr value 1'}, + 'text': '', + 'tail': '', + 'sub_elts': [{'tag': 'subroot1', + 'attrib': {'subattr1': 'sub attr value 1'}, + 'text': '', + 'tail': '', + 'sub_elts': [{'tag': 'subroot2', + 'attrib': {'subattr2': 'sub attr value 2'}, + 'text': 'subtext 2', + 'tail': '', + 'sub_elts': ''}] + }] + } +XML_STR_3 = (b'' + b'' + b'' + b'subtext 2' + b'') +FLAT_MULT = {'root1|attr1="my attr value 1"|attr2="my attr value 2"': 'root text', + 'root2|attr1="my attr value 1"': + {'subroot1|subattr1="sub attr value 1"': 'subtext 1'} + } +ETREE_DICT_MULT = [{'tag': 'root2', + 'attrib': {'attr1': 'my attr value 1'}, + 'text': '', + 'tail': '', + 'sub_elts': [{'tag': 'subroot1', + 'attrib': {'subattr1': 'sub attr value 1'}, + 'text': 'subtext 1', + 'tail': '', + 'sub_elts': ''}] + }, + {'tag': 'root1', + 'attrib': {'attr1': 'my attr value 1', + 'attr2': 'my attr value 2'}, + 'text': 'root text', + 'tail': '', + 'sub_elts': ''}] + +FLAT_MULT_SUB = {'root|attr1="val 1"': {'subroot1|attr1="sub val 1"': 'sub text 1', + 'subroot2|attr2="sub val 2"': 'sub text 2'}} + +ETREE_DICT_MULT_SUB = {'tag': 'root', + 'attrib': {'attr1': 'val 1'}, + 'text': '', + 'tail': '', + 'sub_elts': [{'tag': 'subroot1', + 'attrib': {'attr1': 'sub val 1'}, + 'text': 'sub text 1', + 'tail': '', + 'sub_elts': ''}, + {'tag': 'subroot2', + 'attrib': {'attr2': 'sub val 2'}, + 'text': 'sub text 2', + 'tail': '', + 'sub_elts': ''}] + } +XML_STR_MULT_SUB = (b'' + b'sub text 1' + b'sub text 2' + b'') + + +class TestFlatToEtree(unittest.TestCase): + + def test_raises_exception_when_input_is_wrong(self): + d = {'abc'} + + self.assertRaises(Exception, flat_dict_to_etree_elt_dict, d) + + def test_raises_exception_when_child_type_is_wrong(self): + d = {'root2|attr1="my attr value 1"': [ + 'subroot1|subattr1="sub attr value 1"', 'subtext 1' + ] + } + + self.assertRaises(Exception, flat_dict_to_etree_elt_dict, d) + + def test_one_level(self): + d = FLAT_1 + expected_out = ETREE_DICT_1 + + out = flat_dict_to_etree_elt_dict(d) + + self.assertEqual(out, expected_out) + + def test_two_levels(self): + d = FLAT_2 + expected_out = ETREE_DICT_2 + + out = flat_dict_to_etree_elt_dict(d) + + self.assertEqual(out, expected_out) + + def test_three_levels(self): + d = FLAT_3 + expected_out = ETREE_DICT_3 + + out = flat_dict_to_etree_elt_dict(d) + + self.assertEqual(out, expected_out) + + def test_multiple_root_nodes_raises_exception(self): + d = FLAT_MULT + + self.assertRaises(Exception, etree_elt_dict_to_xml, d) + + def test_multiple_sub_nodes(self): + d = FLAT_MULT_SUB + expected_out = ETREE_DICT_MULT_SUB + + out = flat_dict_to_etree_elt_dict(d) + + for key in out.keys(): + if key != 'sub_elts': + self.assertEqual(out[key], expected_out[key]) + for d in out['sub_elts']: + self.assertIn(d, expected_out['sub_elts']) + + +class TestEtreeEltDictToXml(unittest.TestCase): + + def test_raises_exception_when_input_is_wrong(self): + d = ['abc'] + + self.assertRaises(Exception, etree_elt_dict_to_xml, d) + + def test_one_level(self): + d = ETREE_DICT_1 + expected_out = XML_STR_1 + + out = etree_elt_dict_to_xml(d) + + self.assertEqual(out, expected_out) + + def test_two_levels(self): + d = ETREE_DICT_2 + expected_out = XML_STR_2 + + out = etree_elt_dict_to_xml(d) + + self.assertEqual(out, expected_out) + + def test_three_levels(self): + d = ETREE_DICT_3 + expected_out = XML_STR_3 + + out = etree_elt_dict_to_xml(d) + + self.assertEqual(out, expected_out) + + def test_multiple_root_nodes_raises_exception(self): + d = ETREE_DICT_MULT + + self.assertRaises(Exception, etree_elt_dict_to_xml, d) + + def test_multiple_sub_nodes(self): + d = ETREE_DICT_MULT_SUB + expected_out = XML_STR_MULT_SUB + + out = etree_elt_dict_to_xml(d) + + self.assertEqual(out, expected_out) # potential sequencing issue + + +class TestEtreeNodeToEltDict(unittest.TestCase): + + def test_etree_node_to_etree_elt_dict(self): + pass + + +if __name__ == '__main__': + unittest.main() From 1632a85fb38008f1ac399ff77aa07cf30acfc881 Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Sun, 22 Nov 2015 14:10:08 -0500 Subject: [PATCH 2/6] converted empty string to None per etree convention, XML instead of Xml naming on mixins --- tapioca/adapters.py | 6 ++-- tapioca/xml_helpers.py | 18 ++++++----- tests/test_xml_helpers.py | 66 ++++++++++++++++++++++----------------- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/tapioca/adapters.py b/tapioca/adapters.py index 167db2a..b1e68d2 100644 --- a/tapioca/adapters.py +++ b/tapioca/adapters.py @@ -113,10 +113,10 @@ def response_to_native(self, response): return response.json() -class XmlAdapterMixin(object): +class XMLAdapterMixin(object): def get_request_kwargs(self, api_params, *args, **kwargs): - arguments = super(XmlAdapterMixin, self).get_request_kwargs( + arguments = super(XMLAdapterMixin, self).get_request_kwargs( api_params, *args, **kwargs) if 'headers' not in arguments: @@ -136,7 +136,7 @@ def response_to_native(self, response): return {'text': response.text} -class FlatXmlAdapterMixin(XmlAdapterMixin): +class FlatXMLAdapterMixin(XMLAdapterMixin): def format_data_to_request(self, data): if data: diff --git a/tapioca/xml_helpers.py b/tapioca/xml_helpers.py index db1e82f..fcca038 100644 --- a/tapioca/xml_helpers.py +++ b/tapioca/xml_helpers.py @@ -32,17 +32,19 @@ def flat_dict_to_etree_elt_dict(dict_to_convert, _depth=0): # no support for text and subelements for any single node. if isinstance(v, Mapping): - etree_elt_dict['text'] = '' + etree_elt_dict['text'] = None etree_elt_dict['sub_elts'] = flat_dict_to_etree_elt_dict(dict_to_convert=v, _depth=_depth + 1) elif isinstance(v, str): etree_elt_dict['text'] = v - etree_elt_dict['sub_elts'] = '' + etree_elt_dict['sub_elts'] = None else: raise Exception('Child element not of type Mapping or String') - etree_elt_dict['tail'] = '' + etree_elt_dict['tail'] = None node_list.append(etree_elt_dict) + if not node_list: + node_list = None return node_list if _depth else node_list[0] @@ -84,11 +86,11 @@ def etree_node_to_etree_elt_dict(etree_node): etree_elt_dict['text'] = etree_node.text etree_elt_dict['tail'] = etree_node.tail - sub_elts = [] - for child_node in etree_node: - sub_elts.append(etree_node_to_etree_elt_dict(child_node)) - etree_elt_dict['sub_elts'] = sub_elts - + sub_elts = [etree_node_to_etree_elt_dict(n) for n in etree_node] + if sub_elts: + etree_elt_dict['sub_elts'] = sub_elts + else: + etree_elt_dict['sub_elts'] = None return etree_elt_dict diff --git a/tests/test_xml_helpers.py b/tests/test_xml_helpers.py index fec58ce..4b23c39 100644 --- a/tests/test_xml_helpers.py +++ b/tests/test_xml_helpers.py @@ -2,8 +2,8 @@ import unittest -from tapioca.xml_helpers import flat_dict_to_etree_elt_dict -from tapioca.xml_helpers import etree_elt_dict_to_xml +from tapioca.xml_helpers import ( + etree_elt_dict_to_xml, flat_dict_to_etree_elt_dict, xml_string_to_etree_elt_dict) # Use sets of 3 to test the translation methods @@ -14,21 +14,21 @@ 'attrib': {'attr1': 'my attr value 1', 'attr2': 'my attr value 2'}, 'text': 'root text', - 'tail': '', - 'sub_elts': ''} + 'tail': None, + 'sub_elts': None} XML_STR_1 = b'root text' FLAT_2 = {'root2|attr1="my attr value 1"': { 'subroot1|subattr1="sub attr value 1"': 'subtext 1'}} ETREE_DICT_2 = {'tag': 'root2', 'attrib': {'attr1': 'my attr value 1'}, - 'text': '', - 'tail': '', + 'text': None, + 'tail': None, 'sub_elts': [{'tag': 'subroot1', 'attrib': {'subattr1': 'sub attr value 1'}, 'text': 'subtext 1', - 'tail': '', - 'sub_elts': ''}] + 'tail': None, + 'sub_elts': None}] } XML_STR_2 = (b'' b'subtext 1') @@ -39,17 +39,17 @@ }} ETREE_DICT_3 = {'tag': 'root2', 'attrib': {'attr1': 'my attr value 1'}, - 'text': '', - 'tail': '', + 'text': None, + 'tail': None, 'sub_elts': [{'tag': 'subroot1', 'attrib': {'subattr1': 'sub attr value 1'}, - 'text': '', - 'tail': '', + 'text': None, + 'tail': None, 'sub_elts': [{'tag': 'subroot2', 'attrib': {'subattr2': 'sub attr value 2'}, 'text': 'subtext 2', - 'tail': '', - 'sub_elts': ''}] + 'tail': None, + 'sub_elts': None}] }] } XML_STR_3 = (b'' @@ -63,38 +63,38 @@ } ETREE_DICT_MULT = [{'tag': 'root2', 'attrib': {'attr1': 'my attr value 1'}, - 'text': '', - 'tail': '', + 'text': None, + 'tail': None, 'sub_elts': [{'tag': 'subroot1', 'attrib': {'subattr1': 'sub attr value 1'}, 'text': 'subtext 1', - 'tail': '', - 'sub_elts': ''}] + 'tail': None, + 'sub_elts': None}] }, {'tag': 'root1', 'attrib': {'attr1': 'my attr value 1', 'attr2': 'my attr value 2'}, 'text': 'root text', - 'tail': '', - 'sub_elts': ''}] + 'tail': None, + 'sub_elts': None}] FLAT_MULT_SUB = {'root|attr1="val 1"': {'subroot1|attr1="sub val 1"': 'sub text 1', 'subroot2|attr2="sub val 2"': 'sub text 2'}} ETREE_DICT_MULT_SUB = {'tag': 'root', 'attrib': {'attr1': 'val 1'}, - 'text': '', - 'tail': '', + 'text': None, + 'tail': None, 'sub_elts': [{'tag': 'subroot1', 'attrib': {'attr1': 'sub val 1'}, 'text': 'sub text 1', - 'tail': '', - 'sub_elts': ''}, + 'tail': None, + 'sub_elts': None}, {'tag': 'subroot2', 'attrib': {'attr2': 'sub val 2'}, 'text': 'sub text 2', - 'tail': '', - 'sub_elts': ''}] + 'tail': None, + 'sub_elts': None}] } XML_STR_MULT_SUB = (b'' b'sub text 1' @@ -204,11 +204,19 @@ def test_multiple_sub_nodes(self): self.assertEqual(out, expected_out) # potential sequencing issue -class TestEtreeNodeToEltDict(unittest.TestCase): +class TestXmlStringToEtreeEltDict(unittest.TestCase): - def test_etree_node_to_etree_elt_dict(self): - pass + def test_xml_string_to_etree_elt_dict(self): + xml = XML_STR_MULT_SUB + expected_out = ETREE_DICT_MULT_SUB + + out = xml_string_to_etree_elt_dict(xml) + for key in out.keys(): + if key != 'sub_elts': + self.assertEqual(out[key], expected_out[key]) + for d in out['sub_elts']: + self.assertIn(d, expected_out['sub_elts']) if __name__ == '__main__': unittest.main() From f782f1fde8e3da296b5fdd147c912818bd5d7511 Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Sun, 22 Nov 2015 19:16:54 -0500 Subject: [PATCH 3/6] single XML mixin class with input branching, added support for etree Element and xml strings as input, dual response output for XML and dict --- tapioca/adapters.py | 14 +++------ tapioca/xml_helpers.py | 22 ++++++++++++-- tests/test_xml_helpers.py | 64 ++++++++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/tapioca/adapters.py b/tapioca/adapters.py index b1e68d2..a35083b 100644 --- a/tapioca/adapters.py +++ b/tapioca/adapters.py @@ -7,7 +7,7 @@ ResponseProcessException, ClientError, ServerError) from .serializers import SimpleSerializer from .xml_helpers import ( - etree_elt_dict_to_xml, flat_dict_to_etree_elt_dict, xml_string_to_etree_elt_dict) + input_branches_to_xml_bytestring, xml_string_to_etree_elt_dict) def generate_wrapper_from_adapter(adapter_class): @@ -127,17 +127,11 @@ def get_request_kwargs(self, api_params, *args, **kwargs): def format_data_to_request(self, data): if data: - return etree_elt_dict_to_xml(data) + return input_branches_to_xml_bytestring(data) def response_to_native(self, response): if response.content.strip(): if 'xml' in response.headers['content-type']: - return xml_string_to_etree_elt_dict(response.content) + return {'xml': response.content, + 'dict': xml_string_to_etree_elt_dict(response.content)} return {'text': response.text} - - -class FlatXMLAdapterMixin(XMLAdapterMixin): - - def format_data_to_request(self, data): - if data: - return etree_elt_dict_to_xml(flat_dict_to_etree_elt_dict(data)) diff --git a/tapioca/xml_helpers.py b/tapioca/xml_helpers.py index fcca038..5b37305 100644 --- a/tapioca/xml_helpers.py +++ b/tapioca/xml_helpers.py @@ -79,14 +79,15 @@ def etree_elt_dict_to_xml(etree_elt_dict): ) -def etree_node_to_etree_elt_dict(etree_node): +def _etree_node_to_etree_elt_dict(etree_node): + # for output etree_elt_dict = {} etree_elt_dict['tag'] = etree_node.tag etree_elt_dict['attrib'] = etree_node.attrib etree_elt_dict['text'] = etree_node.text etree_elt_dict['tail'] = etree_node.tail - sub_elts = [etree_node_to_etree_elt_dict(n) for n in etree_node] + sub_elts = [_etree_node_to_etree_elt_dict(n) for n in etree_node] if sub_elts: etree_elt_dict['sub_elts'] = sub_elts else: @@ -95,4 +96,19 @@ def etree_node_to_etree_elt_dict(etree_node): def xml_string_to_etree_elt_dict(xml_string): - return etree_node_to_etree_elt_dict(ElementTree.fromstring(xml_string)) + # for output + return _etree_node_to_etree_elt_dict(ElementTree.fromstring(xml_string)) + + +def input_branches_to_xml_bytestring(data): + if type(data) == ElementTree.Element: + return bytes(ElementTree.tostring(data)) + elif type(data) in (str, bytes): + return bytes(data) + elif type(data) == dict: + if 'tag' in data.keys(): + return bytes(etree_elt_dict_to_xml(data)) + else: + return bytes(etree_elt_dict_to_xml(flat_dict_to_etree_elt_dict(data))) + else: + raise Exception('Format not recognized') diff --git a/tests/test_xml_helpers.py b/tests/test_xml_helpers.py index 4b23c39..e677ce4 100644 --- a/tests/test_xml_helpers.py +++ b/tests/test_xml_helpers.py @@ -1,9 +1,11 @@ # coding: utf-8 import unittest - +from collections import OrderedDict +from xml.etree import ElementTree from tapioca.xml_helpers import ( - etree_elt_dict_to_xml, flat_dict_to_etree_elt_dict, xml_string_to_etree_elt_dict) + etree_elt_dict_to_xml, flat_dict_to_etree_elt_dict, xml_string_to_etree_elt_dict, + input_branches_to_xml_bytestring) # Use sets of 3 to test the translation methods @@ -78,8 +80,8 @@ 'tail': None, 'sub_elts': None}] -FLAT_MULT_SUB = {'root|attr1="val 1"': {'subroot1|attr1="sub val 1"': 'sub text 1', - 'subroot2|attr2="sub val 2"': 'sub text 2'}} +FLAT_MULT_SUB = {'root|attr1="val 1"': OrderedDict([('subroot1|attr1="sub val 1"', 'sub text 1'), + ('subroot2|attr2="sub val 2"', 'sub text 2')])} ETREE_DICT_MULT_SUB = {'tag': 'root', 'attrib': {'attr1': 'val 1'}, @@ -155,11 +157,11 @@ def test_multiple_sub_nodes(self): for key in out.keys(): if key != 'sub_elts': self.assertEqual(out[key], expected_out[key]) - for d in out['sub_elts']: - self.assertIn(d, expected_out['sub_elts']) + self.assertEqual(out['sub_elts'][0], expected_out['sub_elts'][0]) + self.assertEqual(out['sub_elts'][1], expected_out['sub_elts'][1]) -class TestEtreeEltDictToXml(unittest.TestCase): +class TestEtreeEltDictToXML(unittest.TestCase): def test_raises_exception_when_input_is_wrong(self): d = ['abc'] @@ -201,10 +203,50 @@ def test_multiple_sub_nodes(self): out = etree_elt_dict_to_xml(d) - self.assertEqual(out, expected_out) # potential sequencing issue + self.assertEqual(out, expected_out) + + +class TestXMLInputBranches(unittest.TestCase): + + def test_branch_etree_element(self): + elt = ElementTree.fromstring(XML_STR_MULT_SUB) + expected_out = XML_STR_MULT_SUB + + out = input_branches_to_xml_bytestring(elt) + + self.assertEqual(out, expected_out) + + def test_branch_xml_string(self): + xml = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB + + out = input_branches_to_xml_bytestring(xml) + + self.assertEqual(out, expected_out) + + def test_branch_etree_elt_dict(self): + d = ETREE_DICT_MULT_SUB + expected_out = XML_STR_MULT_SUB + + out = input_branches_to_xml_bytestring(d) + + self.assertEqual(out, expected_out) + + def test_branch_flat_dict(self): + d = FLAT_MULT_SUB + expected_out = XML_STR_MULT_SUB + + out = input_branches_to_xml_bytestring(d) + + self.assertEqual(out, expected_out) + + def test_raises_exception_if_wrong_input(self): + d = ['abc'] + + self.assertRaises(Exception, input_branches_to_xml_bytestring, d) -class TestXmlStringToEtreeEltDict(unittest.TestCase): +class TestXMLStringToEtreeEltDict(unittest.TestCase): def test_xml_string_to_etree_elt_dict(self): xml = XML_STR_MULT_SUB @@ -215,8 +257,8 @@ def test_xml_string_to_etree_elt_dict(self): for key in out.keys(): if key != 'sub_elts': self.assertEqual(out[key], expected_out[key]) - for d in out['sub_elts']: - self.assertIn(d, expected_out['sub_elts']) + self.assertEqual(out['sub_elts'][0], expected_out['sub_elts'][0]) + self.assertEqual(out['sub_elts'][1], expected_out['sub_elts'][1]) if __name__ == '__main__': unittest.main() From 41eafd4ed29a3e7cf6a0ad4b4987ae4c1ab8a1ae Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Sun, 22 Nov 2015 23:04:52 -0500 Subject: [PATCH 4/6] utf-8 encoding --- tapioca/xml_helpers.py | 12 +++++------- tests/test_xml_helpers.py | 40 +++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/tapioca/xml_helpers.py b/tapioca/xml_helpers.py index 5b37305..77547c9 100644 --- a/tapioca/xml_helpers.py +++ b/tapioca/xml_helpers.py @@ -74,9 +74,7 @@ def etree_elt_dict_to_xml(etree_elt_dict): ''' if not isinstance(etree_elt_dict, Mapping): raise Exception('Structure must be a Mapping object') - return bytes( - ElementTree.tostring(_etree_elt_list_to_xml([etree_elt_dict])[0]) - ) + return ElementTree.tostring(_etree_elt_list_to_xml([etree_elt_dict])[0], encoding='utf-8') def _etree_node_to_etree_elt_dict(etree_node): @@ -102,13 +100,13 @@ def xml_string_to_etree_elt_dict(xml_string): def input_branches_to_xml_bytestring(data): if type(data) == ElementTree.Element: - return bytes(ElementTree.tostring(data)) + return ElementTree.tostring(data, encoding='utf-8') elif type(data) in (str, bytes): - return bytes(data) + return data.encode('utf-8') elif type(data) == dict: if 'tag' in data.keys(): - return bytes(etree_elt_dict_to_xml(data)) + return etree_elt_dict_to_xml(data) else: - return bytes(etree_elt_dict_to_xml(flat_dict_to_etree_elt_dict(data))) + return etree_elt_dict_to_xml(flat_dict_to_etree_elt_dict(data)) else: raise Exception('Format not recognized') diff --git a/tests/test_xml_helpers.py b/tests/test_xml_helpers.py index e677ce4..40366df 100644 --- a/tests/test_xml_helpers.py +++ b/tests/test_xml_helpers.py @@ -18,7 +18,7 @@ 'text': 'root text', 'tail': None, 'sub_elts': None} -XML_STR_1 = b'root text' +XML_STR_1 = 'root text' FLAT_2 = {'root2|attr1="my attr value 1"': { 'subroot1|subattr1="sub attr value 1"': 'subtext 1'}} @@ -32,8 +32,8 @@ 'tail': None, 'sub_elts': None}] } -XML_STR_2 = (b'' - b'subtext 1') +XML_STR_2 = ('' + 'subtext 1') FLAT_3 = {'root2|attr1="my attr value 1"': { 'subroot1|subattr1="sub attr value 1"': { @@ -54,11 +54,11 @@ 'sub_elts': None}] }] } -XML_STR_3 = (b'' - b'' - b'' - b'subtext 2' - b'') +XML_STR_3 = ('' + '' + '' + 'subtext 2' + '') FLAT_MULT = {'root1|attr1="my attr value 1"|attr2="my attr value 2"': 'root text', 'root2|attr1="my attr value 1"': {'subroot1|subattr1="sub attr value 1"': 'subtext 1'} @@ -98,10 +98,10 @@ 'tail': None, 'sub_elts': None}] } -XML_STR_MULT_SUB = (b'' - b'sub text 1' - b'sub text 2' - b'') +XML_STR_MULT_SUB = ('' + 'sub text 1' + 'sub text 2' + '') class TestFlatToEtree(unittest.TestCase): @@ -170,7 +170,7 @@ def test_raises_exception_when_input_is_wrong(self): def test_one_level(self): d = ETREE_DICT_1 - expected_out = XML_STR_1 + expected_out = XML_STR_1.encode('utf-8') out = etree_elt_dict_to_xml(d) @@ -178,7 +178,7 @@ def test_one_level(self): def test_two_levels(self): d = ETREE_DICT_2 - expected_out = XML_STR_2 + expected_out = XML_STR_2.encode('utf-8') out = etree_elt_dict_to_xml(d) @@ -186,7 +186,7 @@ def test_two_levels(self): def test_three_levels(self): d = ETREE_DICT_3 - expected_out = XML_STR_3 + expected_out = XML_STR_3.encode('utf-8') out = etree_elt_dict_to_xml(d) @@ -199,7 +199,7 @@ def test_multiple_root_nodes_raises_exception(self): def test_multiple_sub_nodes(self): d = ETREE_DICT_MULT_SUB - expected_out = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB.encode('utf-8') out = etree_elt_dict_to_xml(d) @@ -210,7 +210,7 @@ class TestXMLInputBranches(unittest.TestCase): def test_branch_etree_element(self): elt = ElementTree.fromstring(XML_STR_MULT_SUB) - expected_out = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB.encode('utf-8') out = input_branches_to_xml_bytestring(elt) @@ -218,7 +218,7 @@ def test_branch_etree_element(self): def test_branch_xml_string(self): xml = XML_STR_MULT_SUB - expected_out = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB.encode('utf-8') out = input_branches_to_xml_bytestring(xml) @@ -226,7 +226,7 @@ def test_branch_xml_string(self): def test_branch_etree_elt_dict(self): d = ETREE_DICT_MULT_SUB - expected_out = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB.encode('utf-8') out = input_branches_to_xml_bytestring(d) @@ -234,7 +234,7 @@ def test_branch_etree_elt_dict(self): def test_branch_flat_dict(self): d = FLAT_MULT_SUB - expected_out = XML_STR_MULT_SUB + expected_out = XML_STR_MULT_SUB.encode('utf-8') out = input_branches_to_xml_bytestring(d) From 7bb1dbf5a3472086e302aa12c3e72eef131d24fd Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Sun, 22 Nov 2015 23:31:41 -0500 Subject: [PATCH 5/6] fix for python3 bytes input --- tapioca/xml_helpers.py | 4 +++- tests/test_xml_helpers.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tapioca/xml_helpers.py b/tapioca/xml_helpers.py index 77547c9..584a86e 100644 --- a/tapioca/xml_helpers.py +++ b/tapioca/xml_helpers.py @@ -101,8 +101,10 @@ def xml_string_to_etree_elt_dict(xml_string): def input_branches_to_xml_bytestring(data): if type(data) == ElementTree.Element: return ElementTree.tostring(data, encoding='utf-8') - elif type(data) in (str, bytes): + elif type(data) == str: return data.encode('utf-8') + elif type(data) == bytes: + return data elif type(data) == dict: if 'tag' in data.keys(): return etree_elt_dict_to_xml(data) diff --git a/tests/test_xml_helpers.py b/tests/test_xml_helpers.py index 40366df..b32eebb 100644 --- a/tests/test_xml_helpers.py +++ b/tests/test_xml_helpers.py @@ -224,6 +224,14 @@ def test_branch_xml_string(self): self.assertEqual(out, expected_out) + def test_branch_xml_bytes(self): + xml = XML_STR_MULT_SUB.encode('utf-8') + expected_out = XML_STR_MULT_SUB.encode('utf-8') + + out = input_branches_to_xml_bytestring(xml) + + self.assertEqual(out, expected_out) + def test_branch_etree_elt_dict(self): d = ETREE_DICT_MULT_SUB expected_out = XML_STR_MULT_SUB.encode('utf-8') From 31237edc7770208d848a3f2fb7272eb13ebdf25d Mon Sep 17 00:00:00 2001 From: Will Plaehn Date: Mon, 23 Nov 2015 00:17:52 -0500 Subject: [PATCH 6/6] abstracted token requester from user prompts --- tapioca/oauth2_token_requester.py | 35 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tapioca/oauth2_token_requester.py b/tapioca/oauth2_token_requester.py index 0335f78..084c4af 100644 --- a/tapioca/oauth2_token_requester.py +++ b/tapioca/oauth2_token_requester.py @@ -20,31 +20,30 @@ def __init__(self, self.scope_list = scope_list self.auth_base_url_kwargs = auth_base_url_kwargs - def request_token(self): + def authorize_app(self): ''' Uses requests_oauthlib to request a token and response. Requires your app to have a redirect_uri set up. - Prompts user through steps of obtaining a token. - Usage: - o = OauthRequester(**kwargs) # '**kwargs' for brevity. - token = o.request_token() + o = Oauth2TokenRequester(**kwargs) # '**kwargs' for brevity. + authorization_url = o.authorize_app() + # go to authorization_url to perform authorization with third party + # record redirect_response + token = o.get_token(redirect_response) ''' - oauth = OAuth2Session(self.client_id, - scope=self.scope_list, - redirect_uri=self.redirect_uri) - authorization_url, state = oauth.authorization_url(self.authorization_base_url, - **self.auth_base_url_kwargs) + self.oauth = OAuth2Session(self.client_id, + scope=self.scope_list, + redirect_uri=self.redirect_uri) + authorization_url, state = self.oauth.authorization_url(self.authorization_base_url, + **self.auth_base_url_kwargs) - print ('\n###### OauthRequester User Prompt ######\n' - '1. Please go to the following URL to authorize access: \n\n%s' % authorization_url) + return authorization_url - redirect_response = raw_input('\n2. Enter the full callback URL that your request was ' - 'redirected to:\n') + def get_token(self, redirect_response): - oauth.fetch_token(self.obtain_token_url, - authorization_response=redirect_response, - client_secret=self.client_secret) + self.oauth.fetch_token(self.obtain_token_url, + authorization_response=redirect_response, + client_secret=self.client_secret) - return oauth.token + return self.oauth.token