From 9b740de638579d71184f609383a0ec8d52e79071 Mon Sep 17 00:00:00 2001 From: Vadim Pavlov Date: Fri, 16 Feb 2018 01:12:37 +0900 Subject: [PATCH 1/6] Add id argument to Manager.save & save_or_put methods. --- README.md | 3 ++ tests/manager.py | 109 +++++++++++++++++++++++++++++++++++++++++++- xero/__init__.py | 2 +- xero/basemanager.py | 8 ++-- 4 files changed, 117 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a010502f..95d49eb9 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,9 @@ For example, to deal with contacts:: # Save multiple objects >>> xero.contacts.save([c1, c2]) + +# Delete an existing payment +>>> xero.payments.save({'Status': 'DELETED'}, id='b05466c8-dc54-4ff8-8f17-9d7008a2e44b') ``` Complex filters can be constructed in the Django-way, for example retrieving invoices for a contact: diff --git a/tests/manager.py b/tests/manager.py index 67903ca9..cb785a78 100644 --- a/tests/manager.py +++ b/tests/manager.py @@ -6,11 +6,13 @@ import unittest from collections import defaultdict -from mock import Mock +from mock import patch, Mock from xml.dom.minidom import parseString from xero.manager import Manager +from . import mock_data + class ManagerTest(unittest.TestCase): def assertXMLEqual(self, xml1, xml2, message=''): @@ -363,3 +365,108 @@ def test_get_params(self): "toDate": "2015-01-15", "unitdp": 4, }, "test params respects existing values") + + @patch('requests.post') + def test_save_should_issue_a_post_request(self, r_post): + """The save method should issue a post request.""" + + r_post.return_value = Mock( + status_code=200, + headers={ + 'content-type': 'text/xml; charset=utf-8' + }, + text='' + ) + + credentials = Mock(base_url="https://api.xero.com") + + manager = Manager("invoices", credentials) + + manager.save({}) + + r_post.assert_called_once() + + @patch('requests.post') + def test_save_should_post_to_api_url(self, r_post): + """The save method should issue a post request to a uri combined from + the credentials.base_url, Manager.name and optional 'id' argument. + """ + + r_post.return_value = Mock( + status_code=200, + headers={ + 'content-type': 'text/xml; charset=utf-8' + }, + text='' + ) + + credentials = Mock(base_url="https://api.xero.com") + + manager = Manager("invoices", credentials) + + manager.save({}) + + self.assertEqual( + r_post.call_args[0], + ('https://api.xero.com/api.xro/2.0/invoices', )) + + r_post.reset_mock() + + manager.save({}, id='8694c9c5-7097-4449-a708-b8c1982921a4') + + self.assertEqual( + r_post.call_args[0], + ('https://api.xero.com/api.xro/2.0/invoices/' + '8694c9c5-7097-4449-a708-b8c1982921a4', )) + + @patch('requests.post') + def test_save_should_post_data_in_xml_format(self, r_post): + """The save method should post the input data in XML format. + """ + + r_post.return_value = Mock( + status_code=200, + headers={ + 'content-type': 'text/xml; charset=utf-8' + }, + text='' + ) + + credentials = Mock(base_url="https://api.xero.com") + + manager = Manager("invoices", credentials) + + manager.save({ + 'Type': 'ACCREC', + 'Contact': { + 'Name': 'Martin Hudson', + }, + 'Date': datetime.date(2013, 4, 29), + 'DueDate': datetime.date(2013, 4, 29), + 'LineAmountTypes': 'Exclusive', + 'LineItems': [ + { + 'Description': 'Monthly rental', + 'Quantity': 4.34, + 'UnitAmount': 395.0, + 'AccountCode': 200 + } + ] + }) + + self.assertXMLEqual( + r_post.call_args[1]['data']['xml'], + 'Martin Hudson' + '2013-04-29T00:00:00' + 'Exclusive' + '' + '' + '200' + '395.0' + 'Monthly rental' + '4.34' + '' + '' + 'ACCREC' + '2013-04-29T00:00:00' + ) diff --git a/xero/__init__.py b/xero/__init__.py index bbbf0beb..642280c6 100644 --- a/xero/__init__.py +++ b/xero/__init__.py @@ -1,4 +1,4 @@ from .api import Xero -__version__ = "0.9.0" +__version__ = "0.9.1" diff --git a/xero/basemanager.py b/xero/basemanager.py index 57234f4b..be9a53b3 100644 --- a/xero/basemanager.py +++ b/xero/basemanager.py @@ -259,16 +259,18 @@ def get_attachment(self, id, filename, file): file.write(data) return len(data) - def save_or_put(self, data, method='post', headers=None, summarize_errors=True): + def save_or_put(self, data, method='post', headers=None, summarize_errors=True, id=None): uri = '/'.join([self.base_url, self.name]) + if id is not None: + uri += '/' + id body = {'xml': self._prepare_data_for_save(data)} params = self.extra_params.copy() if not summarize_errors: params['summarizeErrors'] = 'false' return uri, params, method, body, headers, False - def _save(self, data): - return self.save_or_put(data, method='post') + def _save(self, data, id=None): + return self.save_or_put(data, method='post', id=id) def _put(self, data, summarize_errors=True): return self.save_or_put(data, method='put', summarize_errors=summarize_errors) From 8b64f69fb9ccfd249cf7cdb58d2263c4be4f8759 Mon Sep 17 00:00:00 2001 From: Vadim Pavlov Date: Fri, 16 Feb 2018 01:16:17 +0900 Subject: [PATCH 2/6] Remove an unused import. --- tests/manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/manager.py b/tests/manager.py index cb785a78..f5a12a47 100644 --- a/tests/manager.py +++ b/tests/manager.py @@ -11,8 +11,6 @@ from xero.manager import Manager -from . import mock_data - class ManagerTest(unittest.TestCase): def assertXMLEqual(self, xml1, xml2, message=''): From e907a9d4a575118b6f37ae25e48b0ccc7998a9b7 Mon Sep 17 00:00:00 2001 From: Vadim Pavlov Date: Sat, 9 May 2020 23:06:14 +1000 Subject: [PATCH 3/6] Added PaymentManager with a custom delete method. --- README.md | 3 --- tests/helpers.py | 29 +++++++++++++++++++++++++++++ tests/manager.py | 41 ++++++++--------------------------------- tests/paymentmanager.py | 30 ++++++++++++++++++++++++++++++ xero/__init__.py | 2 +- xero/api.py | 9 ++++++++- xero/paymentmanager.py | 29 +++++++++++++++++++++++++++++ 7 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 tests/helpers.py create mode 100644 tests/paymentmanager.py create mode 100644 xero/paymentmanager.py diff --git a/README.md b/README.md index 2a8b6be2..a2b88fab 100644 --- a/README.md +++ b/README.md @@ -390,9 +390,6 @@ For example, to deal with contacts:: # Save multiple objects >>> xero.contacts.save([c1, c2]) - -# Delete an existing payment ->>> xero.payments.save({'Status': 'DELETED'}, id='b05466c8-dc54-4ff8-8f17-9d7008a2e44b') ``` Complex filters can be constructed in the Django-way, for example retrieving invoices for a contact: diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..915a3d3e --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,29 @@ + +import re +import six + +from collections import defaultdict +from xml.dom.minidom import parseString + + +def assertXMLEqual(test_case, xml1, xml2, message=""): + def to_str(s): + return s.decode("utf-8") if six.PY3 and isinstance(s, bytes) else str(s) + + def clean_xml(xml): + xml = "%s" % to_str(xml) + return str(re.sub(">\n *<", "><", parseString(xml).toxml())) + + def xml_to_dict(xml): + nodes = re.findall("(<([^>]*)>(.*?))", xml) + if len(nodes) == 0: + return xml + d = defaultdict(list) + for node in nodes: + d[node[1]].append(xml_to_dict(node[2])) + return d + + cleaned = map(clean_xml, (xml1, xml2)) + d1, d2 = tuple(map(xml_to_dict, cleaned)) + + test_case.assertEqual(d1, d2, message) diff --git a/tests/manager.py b/tests/manager.py index a9bd2de1..80622394 100644 --- a/tests/manager.py +++ b/tests/manager.py @@ -1,38 +1,14 @@ from __future__ import unicode_literals import datetime -import re -import six import unittest -from collections import defaultdict from mock import Mock -from xml.dom.minidom import parseString from xero.manager import Manager +from .helpers import assertXMLEqual class ManagerTest(unittest.TestCase): - def assertXMLEqual(self, xml1, xml2, message=""): - def to_str(s): - return s.decode("utf-8") if six.PY3 and isinstance(s, bytes) else str(s) - - def clean_xml(xml): - xml = "%s" % to_str(xml) - return str(re.sub(">\n *<", "><", parseString(xml).toxml())) - - def xml_to_dict(xml): - nodes = re.findall("(<([^>]*)>(.*?))", xml) - if len(nodes) == 0: - return xml - d = defaultdict(list) - for node in nodes: - d[node[1]].append(xml_to_dict(node[2])) - return d - - cleaned = map(clean_xml, (xml1, xml2)) - d1, d2 = tuple(map(xml_to_dict, cleaned)) - - self.assertEqual(d1, d2, message) def test_serializer(self): credentials = Mock(base_url="") @@ -100,9 +76,7 @@ def test_serializer(self): """ - self.assertXMLEqual( - resultant_xml, expected_xml, - ) + assertXMLEqual(self, resultant_xml, expected_xml) def test_serializer_phones_addresses(self): credentials = Mock(base_url="") @@ -160,8 +134,11 @@ def test_serializer_phones_addresses(self): """ - self.assertXMLEqual( - resultant_xml, expected_xml, "Resultant XML does not match expected." + assertXMLEqual( + self, + resultant_xml, + expected_xml, + "Resultant XML does not match expected." ) def test_serializer_nested_singular(self): @@ -195,9 +172,7 @@ def test_serializer_nested_singular(self): 2015-07-06T16:25:02 """ - self.assertXMLEqual( - resultant_xml, expected_xml, - ) + assertXMLEqual(self, resultant_xml, expected_xml) def test_filter(self): """The filter function should correctly handle various arguments""" diff --git a/tests/paymentmanager.py b/tests/paymentmanager.py new file mode 100644 index 00000000..e3cba5af --- /dev/null +++ b/tests/paymentmanager.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +import unittest +from mock import Mock + +from xero.paymentmanager import PaymentManager +from .helpers import assertXMLEqual + + +class ManagerTest(unittest.TestCase): + + def test_delete(self): + + credentials = Mock(base_url="") + manager = PaymentManager("payments", credentials) + + uri, params, method, body, headers, singleobject = manager._delete( + "768e44ef-c1e3-4d7f-8e06-f6e8bc4eefa4") + + self.assertEqual( + uri, '/api.xro/2.0/payments/768e44ef-c1e3-4d7f-8e06-f6e8bc4eefa4' + ) + + self.assertEqual(params, {}) + self.assertEqual(method, 'post') + self.assertIn('xml', body) + + assertXMLEqual(self, body['xml'], "DELETED") + + self.assertIsNone(headers) diff --git a/xero/__init__.py b/xero/__init__.py index d57b91b5..cd32e369 100644 --- a/xero/__init__.py +++ b/xero/__init__.py @@ -1,3 +1,3 @@ from .api import Xero # NOQA: F401 -__version__ = "0.9.2" +__version__ = "0.9.3" diff --git a/xero/api.py b/xero/api.py index c1d9a149..445c874d 100644 --- a/xero/api.py +++ b/xero/api.py @@ -4,6 +4,7 @@ from .manager import Manager from .payrollmanager import PayrollManager from .projectmanager import ProjectManager +from .paymentmanager import PaymentManager class Xero(object): @@ -46,10 +47,16 @@ def __init__(self, credentials, unit_price_4dps=False, user_agent=None): # the lowercase name of the object and attach it to an # instance of a Manager object to operate on it for name in self.OBJECT_LIST: + + manager_class = Manager + + if name == 'Payments': + manager_class = PaymentManager + setattr( self, name.lower(), - Manager(name, credentials, unit_price_4dps, user_agent), + manager_class(name, credentials, unit_price_4dps, user_agent), ) setattr(self, "filesAPI", Files(credentials)) diff --git a/xero/paymentmanager.py b/xero/paymentmanager.py new file mode 100644 index 00000000..d33b21a8 --- /dev/null +++ b/xero/paymentmanager.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from .basemanager import BaseManager +from .constants import XERO_API_URL +from .utils import resolve_user_agent, singular + + +class PaymentManager(BaseManager): + + def __init__(self, name, credentials, unit_price_4dps=False, user_agent=None): + + self.credentials = credentials + self.name = name + self.base_url = credentials.base_url + XERO_API_URL + self.extra_params = {"unitdp": 4} if unit_price_4dps else {} + self.singular = singular(name) + self.user_agent = resolve_user_agent( + user_agent, getattr(credentials, "user_agent", None) + ) + + for method_name in self.DECORATED_METHODS: + method = getattr(self, "_%s" % method_name) + setattr(self, method_name, self._get_data(method)) + + def _delete(self, id): + uri = "/".join([self.base_url, self.name, id]) + data = {'Status': 'DELETED'} + body = {"xml": self._prepare_data_for_save(data)} + return uri, {}, "post", body, None, False From 152512cc197aaf27b2cb1713e05e35e2495d4080 Mon Sep 17 00:00:00 2001 From: Vadim Pavlov Date: Sat, 9 May 2020 23:18:58 +1000 Subject: [PATCH 4/6] Reoreded imports. --- tests/helpers.py | 1 - tests/manager.py | 1 + tests/paymentmanager.py | 1 + xero/api.py | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/helpers.py b/tests/helpers.py index 915a3d3e..22617e1f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,7 +1,6 @@ import re import six - from collections import defaultdict from xml.dom.minidom import parseString diff --git a/tests/manager.py b/tests/manager.py index 80622394..b4f55a17 100644 --- a/tests/manager.py +++ b/tests/manager.py @@ -5,6 +5,7 @@ from mock import Mock from xero.manager import Manager + from .helpers import assertXMLEqual diff --git a/tests/paymentmanager.py b/tests/paymentmanager.py index e3cba5af..25a7cc1e 100644 --- a/tests/paymentmanager.py +++ b/tests/paymentmanager.py @@ -4,6 +4,7 @@ from mock import Mock from xero.paymentmanager import PaymentManager + from .helpers import assertXMLEqual diff --git a/xero/api.py b/xero/api.py index 445c874d..79572c20 100644 --- a/xero/api.py +++ b/xero/api.py @@ -2,9 +2,9 @@ from .filesmanager import FilesManager from .manager import Manager +from .paymentmanager import PaymentManager from .payrollmanager import PayrollManager from .projectmanager import ProjectManager -from .paymentmanager import PaymentManager class Xero(object): From 728a039e1e67205aa9e4372661eff2aaadd3ef7e Mon Sep 17 00:00:00 2001 From: Vadim Pavlov Date: Sun, 5 Feb 2023 18:37:01 +1100 Subject: [PATCH 5/6] Bumped version to 0.9.4. --- xero/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xero/__init__.py b/xero/__init__.py index cd32e369..a4601d0a 100644 --- a/xero/__init__.py +++ b/xero/__init__.py @@ -1,3 +1,3 @@ from .api import Xero # NOQA: F401 -__version__ = "0.9.3" +__version__ = "0.9.4" From f7d7960308749c4d5902cadef1f2b497eddc957b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 11 Feb 2023 15:28:14 +0800 Subject: [PATCH 6/6] Identify version as development. --- src/xero/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xero/__init__.py b/src/xero/__init__.py index a4601d0a..9c8caa6e 100644 --- a/src/xero/__init__.py +++ b/src/xero/__init__.py @@ -1,3 +1,3 @@ from .api import Xero # NOQA: F401 -__version__ = "0.9.4" +__version__ = "0.9.4.dev0"