From a6ec6ff8a6a7085d63c9a4a0688c43598840a97f Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 12:08:59 -0600 Subject: [PATCH 01/21] Use enumerate instead of manual counter --- pyusps/address_information.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 31964c0..6d627fa 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -56,8 +56,7 @@ def _process_one(address): def _process_multiple(addresses): results = [] - count = 0 - for address in addresses: + for i, address in enumerate(addresses): # Return error object if there are # multiple items error = _get_address_error(address) @@ -65,13 +64,12 @@ def _process_multiple(addresses): result = error else: result = _parse_address(address) - if str(count) != address.get('ID'): + if str(i) != address.get('ID'): msg = ('The addresses returned are not in the same ' 'order they were requested' ) raise IndexError(msg) results.append(result) - count += 1 return results From d1c10d50099a7274de5913bafa3e0a2aa9766aec Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 12:16:19 -0600 Subject: [PATCH 02/21] Add GitHub Actions CI checks --- .github/workflows/test.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..63e87fa --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,29 @@ +name: Tests + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: "0 0 * * 0" # weekly + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + # Pin to an exact 3.9 version due to https://github.com/fudge-py/fudge/issues/13 + python-version: [3.6, 3.7, 3.8, "3.9.6"] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install -e .[test] + - name: Run tests + run: nosetests From d9f4ed5b9ec3080a1a8454bac42db74e6a894724 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 12:22:45 -0600 Subject: [PATCH 03/21] Simplify error parsing --- pyusps/address_information.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 6d627fa..d05d392 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -8,26 +8,23 @@ api_url = 'https://production.shippingapis.com/ShippingAPI.dll' address_max = 5 -def _find_error(root): - if root.tag == 'Error': - num = root.find('Number') - desc = root.find('Description') - return (num, desc) - -def _get_error(error): - (num, desc) = error + +def _get_error(node): + if node.tag != 'Error': + return None return ValueError( '{num}: {desc}'.format( - num=num.text, - desc=desc.text, + num=node.find('Number').text, + desc=node.find('Description').text, ) ) def _get_address_error(address): - error = address.find('Error') - if error is not None: - error = _find_error(error) - return _get_error(error) + error_node = address.find('Error') + if error_node is None: + return None + else: + return _get_error(error_node) def _parse_address(address): result = OrderedDict() @@ -75,9 +72,9 @@ def _process_multiple(addresses): def _parse_response(res): # General error, e.g., authorization - error = _find_error(res.getroot()) + error = _get_error(res.getroot()) if error is not None: - raise _get_error(error) + raise error results = res.findall('Address') length = len(results) From 6533095403a8d35580b27138dccb15250d90ae4c Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 12:34:53 -0600 Subject: [PATCH 04/21] Don't do unneeded work if error --- pyusps/address_information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index d05d392..907f572 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -60,12 +60,12 @@ def _process_multiple(addresses): if error is not None: result = error else: - result = _parse_address(address) if str(i) != address.get('ID'): msg = ('The addresses returned are not in the same ' 'order they were requested' ) raise IndexError(msg) + result = _parse_address(address) results.append(result) return results From ec9ade3a80b21cbebb38d4c7ca8fdb6cc16f194b Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 13:09:26 -0600 Subject: [PATCH 05/21] Accept Iterable, return list, return errors See https://github.com/thelinuxkid/pyusps/issues/10 Switching away from *args makes it so that single inputs are handled consistently. Otherwise, users won't know what sort of response to expect from passing a single input: Will they get a single item back, or a list of length one? Just make it so you always pass a list. Or, in fact, make it so that this accepts any iterable, not just a list or some other iterable with a __len__ method. --- pyusps/address_information.py | 40 +++--- pyusps/test/test_address_information.py | 169 +++++++++--------------- 2 files changed, 81 insertions(+), 128 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 907f572..b302ec8 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from typing import Iterable from lxml import etree @@ -6,7 +7,7 @@ api_url = 'https://production.shippingapis.com/ShippingAPI.dll' -address_max = 5 +ADDRESS_MAX = 5 def _get_error(node): @@ -43,14 +44,6 @@ def _parse_address(address): return result -def _process_one(address): - # Raise address error if there's only one item - error = _get_address_error(address) - if error is not None: - raise error - - return _parse_address(address) - def _process_multiple(addresses): results = [] for i, address in enumerate(addresses): @@ -77,13 +70,10 @@ def _parse_response(res): raise error results = res.findall('Address') - length = len(results) - if length == 0: + if len(results) == 0: raise TypeError( 'Could not find any address or error information' ) - if length == 1: - return _process_one(results.pop()) return _process_multiple(results) def _get_response(xml): @@ -103,21 +93,21 @@ def _get_response(xml): def _create_xml( user_id, - *args + addresses: Iterable, ): root = etree.Element('AddressValidateRequest', USERID=user_id) - if len(args) > address_max: - # Raise here. The Verify API will not return an error. It will - # just return the first 5 results - raise ValueError( - 'Only {address_max} addresses are allowed per ' - 'request'.format( - address_max=address_max, + for i, arg in enumerate(addresses): + if i >= ADDRESS_MAX: + # Raise here. The Verify API will not return an error. It will + # just return the first 5 results + raise ValueError( + 'Only {ADDRESS_MAX} addresses are allowed per ' + 'request'.format( + ADDRESS_MAX=ADDRESS_MAX, + ) ) - ) - for i,arg in enumerate(args): address = arg['address'] city = arg['city'] state = arg.get('state', None) @@ -179,8 +169,8 @@ def _create_xml( return root -def verify(user_id, *args): - xml = _create_xml(user_id, *args) +def verify(user_id: str, addresses: Iterable) -> "list[OrderedDict]": + xml = _create_xml(user_id, addresses) res = _get_response(xml) res = _parse_response(res) diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index fe2b888..91f0deb 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -16,24 +16,21 @@ def test_verify_simple(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -45,24 +42,21 @@ def test_verify_zip5(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -74,24 +68,21 @@ def test_verify_zip_both(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), ('zip_code', '207701441'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -103,24 +94,21 @@ def test_verify_zip_dash(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), ('zip_code', '20770-1441'), - ]) - res = verify( - 'foo_id', - address - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -132,23 +120,20 @@ def test_verify_zip_only(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -160,23 +145,20 @@ def test_verify_state_only(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -188,25 +170,22 @@ def test_verify_firm_name(fake_urlopen):
XYZ CORP6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('firm_name', 'XYZ Corp'), ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('firm_name', 'XYZ CORP'), ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -218,25 +197,22 @@ def test_verify_address_extended(fake_urlopen):
STE 126406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('address_extended', 'Suite 12'), ('city', 'Greenbelt'), ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address_extended', 'STE 12'), ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -248,25 +224,22 @@ def test_verify_urbanization(fake_urlopen):
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('urbanization', 'Puerto Rico'), ('city', 'Greenbelt'), ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + ])] + res = verify('foo_id', address) - expected = OrderedDict([ + expected = [OrderedDict([ ('address', '6406 IVY LN'), ('city', 'GREENBELT'), ('state', 'MD'), ('urbanization', 'PUERTO RICO'), ('zip5', '20770'), ('zip4', '1441'), - ]) + ])] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -290,10 +263,7 @@ def test_verify_multiple(fake_urlopen): ('state', 'CT'), ]), ] - res = verify( - 'foo_id', - *addresses - ) + res = verify('foo_id', addresses) expected = [ OrderedDict([ @@ -315,20 +285,18 @@ def test_verify_multiple(fake_urlopen): @fudge.patch('pyusps.urlutil.urlopen') def test_verify_more_than_5(fake_urlopen): - addresses = [ - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - ] + inp = OrderedDict([ + ('address', '6406 Ivy Lane'), + ('city', 'Greenbelt'), + ('state', 'MD'), + ]) + addresses = [inp] * 6 msg = assert_raises( ValueError, verify, 'foo_id', - *addresses + addresses ) eq(str(msg), 'Only 5 addresses are allowed per request') @@ -354,7 +322,7 @@ def test_verify_api_root_error(fake_urlopen): ValueError, verify, 'foo_id', - address + [address] ) expected = ('80040b1a: Authorization failure. Perhaps username ' @@ -371,20 +339,18 @@ def test_verify_api_address_error_single(fake_urlopen):
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""") fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'NJ'), - ]) - msg = assert_raises( - ValueError, - verify, - 'foo_id', - address - ) + ])] + res = verify('foo_id', address) - expected = '-2147219401: Address Not Found.' - eq(str(msg), expected) + eq(len(res), 1) + assert_errors_equal( + res[0], + ValueError('-2147219401: Address Not Found.'), + ) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_address_error_multiple(fake_urlopen): @@ -407,10 +373,7 @@ def test_verify_api_address_error_multiple(fake_urlopen): ('state', 'NJ'), ]), ] - res = verify( - 'foo_id', - *addresses - ) + res = verify('foo_id', addresses) # eq does not work with exceptions. Process each item manually. eq(len(res), 2) @@ -438,11 +401,11 @@ def test_verify_api_empty_error(fake_urlopen): """) fake_urlopen.returns(res) - address = OrderedDict([ + address = [OrderedDict([ ('address', '6406 Ivy Lane'), ('city', 'Greenbelt'), ('state', 'NJ'), - ]) + ])] msg = assert_raises( TypeError, verify, @@ -478,7 +441,7 @@ def test_verify_api_order_error(fake_urlopen): IndexError, verify, 'foo_id', - *addresses + addresses ) expected = ('The addresses returned are not in the same order ' From f1dd6bcfa854eb4dff791cb7fa7becf4bdbf2d18 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 13:12:50 -0600 Subject: [PATCH 06/21] Move api_url out of global namespace Only used inside this one function, so put it there. --- pyusps/address_information.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index b302ec8..eafb85e 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -5,8 +5,6 @@ import pyusps.urlutil - -api_url = 'https://production.shippingapis.com/ShippingAPI.dll' ADDRESS_MAX = 5 @@ -81,8 +79,7 @@ def _get_response(xml): ('API', 'Verify'), ('XML', etree.tostring(xml)), ]) - url = '{api_url}?{params}'.format( - api_url=api_url, + url = 'https://production.shippingapis.com/ShippingAPI.dll?{params}'.format( params=pyusps.urlutil.urlencode(params), ) From 6c6dce9b20c519a485862c414dc8e8633df4b46d Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 13:17:07 -0600 Subject: [PATCH 07/21] Improve renaming of return fields --- pyusps/address_information.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index eafb85e..4d03bc8 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -27,19 +27,18 @@ def _get_address_error(address): def _parse_address(address): result = OrderedDict() + # More user-friendly names for street + # attributes + m = { + "address2": "address", + "address1": "address_extended", + "firmname": "firm_name", + } for child in address.iterchildren(): # elements are yielded in order name = child.tag.lower() - # More user-friendly names for street - # attributes - if name == 'address2': - name = 'address' - elif name == 'address1': - name = 'address_extended' - elif name == 'firmname': - name = 'firm_name' + name = m.get(name, name) result[name] = child.text - return result def _process_multiple(addresses): From 6f7004e58fea9f849f85b4a12c64c639684f4632 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 13:25:12 -0600 Subject: [PATCH 08/21] Simplify string formatting --- pyusps/address_information.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 4d03bc8..e38952e 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -11,12 +11,7 @@ def _get_error(node): if node.tag != 'Error': return None - return ValueError( - '{num}: {desc}'.format( - num=node.find('Number').text, - desc=node.find('Description').text, - ) - ) + return ValueError(f"{node.find('Number').text}: {node.find('Description').text}") def _get_address_error(address): error_node = address.find('Error') @@ -78,13 +73,10 @@ def _get_response(xml): ('API', 'Verify'), ('XML', etree.tostring(xml)), ]) - url = 'https://production.shippingapis.com/ShippingAPI.dll?{params}'.format( - params=pyusps.urlutil.urlencode(params), - ) - + param_string = pyusps.urlutil.urlencode(params) + url = f'https://production.shippingapis.com/ShippingAPI.dll?{param_string}' res = pyusps.urlutil.urlopen(url) res = etree.parse(res) - return res def _create_xml( @@ -97,12 +89,7 @@ def _create_xml( if i >= ADDRESS_MAX: # Raise here. The Verify API will not return an error. It will # just return the first 5 results - raise ValueError( - 'Only {ADDRESS_MAX} addresses are allowed per ' - 'request'.format( - ADDRESS_MAX=ADDRESS_MAX, - ) - ) + raise ValueError(f'Only {ADDRESS_MAX} addresses are allowed per request') address = arg['address'] city = arg['city'] From 3f28f07c6b9098f61e75d13e660aa078173f35f0 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 14:20:43 -0600 Subject: [PATCH 09/21] Return USPSError, not ValueError See https://github.com/thelinuxkid/pyusps/issues/10 - still allows you to catch ValueErrors - still acts just like a ValueError - Differentiates between errors from USPS, and the ValueError you get if you supply more than 5 addresses --- pyusps/address_information.py | 24 +++++++++- pyusps/test/test_address_information.py | 63 ++++++++++++++----------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index e38952e..0bd0546 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -7,11 +7,33 @@ ADDRESS_MAX = 5 +class USPSError(ValueError): + """ + An error from the USPS API, such as a bad `user_id` or when an address is not found. + + Inherits from ValueError. Also has attributes `code: str` and `description: str`. + """ + code: str + description: str + + def __init__(self, code: str, description: str) -> None: + self.code = code + self.description = description + super().__init__(f"{code}: {description}") + + def __eq__(self, o: object) -> bool: + return ( + isinstance(o, USPSError) and + self.code == o.code and + self.description == o.description + ) def _get_error(node): if node.tag != 'Error': return None - return ValueError(f"{node.find('Number').text}: {node.find('Description').text}") + code = node.find('Number').text + description = node.find('Description').text + return USPSError(code, description) def _get_address_error(address): error_node = address.find('Error') diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 91f0deb..38013aa 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -4,7 +4,7 @@ from nose.tools import eq_ as eq from io import StringIO -from pyusps.address_information import verify +from pyusps.address_information import verify, USPSError from pyusps.test.util import assert_raises, assert_errors_equal @fudge.patch('pyusps.urlutil.urlopen') @@ -292,14 +292,27 @@ def test_verify_more_than_5(fake_urlopen): ]) addresses = [inp] * 6 - msg = assert_raises( + err = assert_raises( ValueError, verify, 'foo_id', addresses ) + expected = ValueError('Only 5 addresses are allowed per request') + assert_errors_equal(err, expected) + + +def test_error_properties(): + """We can treat these pretty much like a ValueError.""" + err = USPSError("code", "description") + err2 = USPSError("code", "description") + eq(err.code, "code") + eq(err.description, "description") + eq(err.args, ("code: description", )) + eq(str(err), "code: description") + assert isinstance(err, ValueError) + eq(err, err2) - eq(str(msg), 'Only 5 addresses are allowed per request') @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_root_error(fake_urlopen): @@ -318,17 +331,18 @@ def test_verify_api_root_error(fake_urlopen): ('city', 'Greenbelt'), ('state', 'MD'), ]) - msg = assert_raises( - ValueError, + err = assert_raises( + USPSError, verify, 'foo_id', [address] ) - expected = ('80040b1a: Authorization failure. Perhaps username ' - 'and/or password is incorrect.' - ) - eq(str(msg), expected) + expected = USPSError( + "80040b1a", + "Authorization failure. Perhaps username and/or password is incorrect." + ) + eq(err, expected) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_address_error_single(fake_urlopen): @@ -347,10 +361,8 @@ def test_verify_api_address_error_single(fake_urlopen): res = verify('foo_id', address) eq(len(res), 1) - assert_errors_equal( - res[0], - ValueError('-2147219401: Address Not Found.'), - ) + expected = USPSError("-2147219401", "Address Not Found.") + eq(res[0], expected) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_address_error_multiple(fake_urlopen): @@ -386,11 +398,9 @@ def test_verify_api_address_error_multiple(fake_urlopen): ('zip5', '20770'), ('zip4', '1441'), ]), - ) - assert_errors_equal( - res[1], - ValueError('-2147219400: Invalid City.'), - ) + ) + expected = USPSError("-2147219400", "Invalid City.") + eq(res[1], expected) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_empty_error(fake_urlopen): @@ -406,15 +416,15 @@ def test_verify_api_empty_error(fake_urlopen): ('city', 'Greenbelt'), ('state', 'NJ'), ])] - msg = assert_raises( + err = assert_raises( TypeError, verify, 'foo_id', address ) - expected = 'Could not find any address or error information' - eq(str(msg), expected) + expected = TypeError('Could not find any address or error information') + assert_errors_equal(err, expected) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_api_order_error(fake_urlopen): @@ -437,14 +447,13 @@ def test_verify_api_order_error(fake_urlopen): ('state', 'CT'), ]), ] - msg = assert_raises( + err = assert_raises( IndexError, verify, 'foo_id', addresses ) - - expected = ('The addresses returned are not in the same order ' - 'they were requested' - ) - eq(str(msg), expected) + expected = IndexError( + 'The addresses returned are not in the same order they were requested' + ) + assert_errors_equal(err, expected) From bf00cf6c221283ea86ca6b6002a8821aafe0f569 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 15:03:32 -0600 Subject: [PATCH 10/21] Use normal dict, not OrderedDict dicts are ordered in any recent version of python, so just use them. This also fixes up the README to be up to date with the previous changes. --- README.rst | 119 ++----- pyusps/address_information.py | 10 +- pyusps/test/test_address_information.py | 428 +++++++++++++----------- 3 files changed, 271 insertions(+), 286 deletions(-) diff --git a/README.rst b/README.rst index faa4ccd..db20f18 100644 --- a/README.rst +++ b/README.rst @@ -22,13 +22,13 @@ or easy_install:: Address Information API ======================= -This API is avaiable via the pyusps.address_information.verify -function. It takes in the user ID given to you by the USPS -and a variable length list of addresses to verify. +This API is avaiable via the `pyusps.address_information.verify` +function. Requests -------- +It takes in the user ID given to you by the USPS and a list of addresses to verify. Each address is a dict containing the following required keys: :address: The street address @@ -49,8 +49,7 @@ The following keys are optional: Responses --------- -The response will either be a dict, if a single address was requested, -or a list of dicts, if multiple addresses were requested. Each address +The response will either be list of dicts. Each address will always contain the following keys: :address: The street address @@ -83,88 +82,40 @@ Instead, if one of the addresses generates an error, the ValueError object is returned along with the rest of the results. -Examples --------- - -Single address request:: - - from pyusps import address_information - - addr = dict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - address_information.verify('foo_id', addr) - dict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ]) - -Mutiple addresses request:: - - from pyusps import address_information - - addrs = [ - dict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - dict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'CT'), - ]), - ] - address_information.verify('foo_id', *addrs) - [ - dict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ]), - dict([ - ('address', '8 WILDWOOD DR'), - ('city', 'OLD LYME'), - ('state', 'CT'), - ('zip5', '06371'), - ('zip4', '1844'), - ]), - ] +Example +------- Mutiple addresses error:: - from pyusps import address_information - - addrs = [ - dict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - dict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'NJ'), - ]), - ] - address_information.verify('foo_id', *addrs) - [ - dict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ]), - ValueError('-2147219400: Invalid City. '), - ] + from pyusps import address_information + + addrs = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + }, + { + "address": "8 Wildwood Drive", + "city": "Old Lyme", + "state": "NJ", + }, + ] + address_information.verify('foo_id', addrs) + [ + { + 'address': '6406 IVY LN', + 'city': 'GREENBELT', + 'returntext': 'Default address: The address you entered was found but more ' + 'information is needed (such as an apartment, suite, or box ' + 'number) to match to a specific address.', + 'state': 'MD', + 'zip4': '1435', + 'zip5': '20770' + }, + USPSError('-2147219400: Invalid City. '), + ] + Reference --------- diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 0bd0546..c02551d 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -1,4 +1,3 @@ -from collections import OrderedDict from typing import Iterable from lxml import etree @@ -43,7 +42,7 @@ def _get_address_error(address): return _get_error(error_node) def _parse_address(address): - result = OrderedDict() + result = {} # More user-friendly names for street # attributes m = { @@ -91,10 +90,7 @@ def _parse_response(res): return _process_multiple(results) def _get_response(xml): - params = OrderedDict([ - ('API', 'Verify'), - ('XML', etree.tostring(xml)), - ]) + params = {'API': 'Verify', 'XML': etree.tostring(xml)} param_string = pyusps.urlutil.urlencode(params) url = f'https://production.shippingapis.com/ShippingAPI.dll?{param_string}' res = pyusps.urlutil.urlopen(url) @@ -174,7 +170,7 @@ def _create_xml( return root -def verify(user_id: str, addresses: Iterable) -> "list[OrderedDict]": +def verify(user_id: str, addresses: Iterable) -> "list[dict]": xml = _create_xml(user_id, addresses) res = _get_response(xml) res = _parse_response(res) diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 38013aa..a6b66e9 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -1,6 +1,5 @@ import fudge -from collections import OrderedDict from nose.tools import eq_ as eq from io import StringIO @@ -16,21 +15,25 @@ def test_verify_simple(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770'), - ])] + address = [ + { + 'address': '6406 Ivy Lane', + 'city': 'Greenbelt', + 'state': 'MD', + 'zip_code': '20770', + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -42,21 +45,25 @@ def test_verify_zip5(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + "zip_code": "20770", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -68,21 +75,25 @@ def test_verify_zip_both(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '207701441'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + "zip_code": "207701441", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -94,21 +105,25 @@ def test_verify_zip_dash(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770-1441'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + "zip_code": "20770-1441", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -120,20 +135,24 @@ def test_verify_zip_only(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('zip_code', '20770'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "zip_code": "20770", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -145,20 +164,24 @@ def test_verify_state_only(fake_urlopen):
6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -170,22 +193,26 @@ def test_verify_firm_name(fake_urlopen):
XYZ CORP6406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('firm_name', 'XYZ Corp'), - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ])] + address = [ + { + "firm_name": "XYZ Corp", + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('firm_name', 'XYZ CORP'), - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "firm_name": "XYZ CORP", + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -197,22 +224,25 @@ def test_verify_address_extended(fake_urlopen):
STE 126406 IVY LNGREENBELTMD207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('address_extended', 'Suite 12'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "address_extended": "Suite 12", + "city": "Greenbelt", + "state": "MD", + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address_extended', 'STE 12'), - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [{ + "address_extended": "STE 12", + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -224,22 +254,26 @@ def test_verify_urbanization(fake_urlopen):
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('urbanization', 'Puerto Rico'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + 'urbanization': 'Puerto Rico', + 'city': 'Greenbelt', + 'state': 'MD', + } + ] res = verify('foo_id', address) - expected = [OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('urbanization', 'PUERTO RICO'), - ('zip5', '20770'), - ('zip4', '1441'), - ])] + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "urbanization": "PUERTO RICO", + "zip5": "20770", + "zip4": "1441", + } + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -252,45 +286,45 @@ def test_verify_multiple(fake_urlopen): fake_urlopen.returns(res) addresses = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'CT'), - ]), - ] + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + }, + { + "address": "8 Wildwood Drive", + "city": "Old Lyme", + "state": "CT", + }, + ] res = verify('foo_id', addresses) expected = [ - OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ]), - OrderedDict([ - ('address', '8 WILDWOOD DR'), - ('city', 'OLD LYME'), - ('state', 'CT'), - ('zip5', '06371'), - ('zip4', '1844'), - ]), - ] + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + }, + { + "address": "8 WILDWOOD DR", + "city": "OLD LYME", + "state": "CT", + "zip5": "06371", + "zip4": "1844", + }, + ] eq(res, expected) @fudge.patch('pyusps.urlutil.urlopen') def test_verify_more_than_5(fake_urlopen): - inp = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - addresses = [inp] * 6 + addr = { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } + addresses = [addr] * 6 err = assert_raises( ValueError, @@ -326,11 +360,11 @@ def test_verify_api_root_error(fake_urlopen): """) fake_urlopen.returns(res) - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) + address = { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } err = assert_raises( USPSError, verify, @@ -353,11 +387,13 @@ def test_verify_api_address_error_single(fake_urlopen):
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""") fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'NJ'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "NJ", + } + ] res = verify('foo_id', address) eq(len(res), 1) @@ -374,30 +410,30 @@ def test_verify_api_address_error_multiple(fake_urlopen): fake_urlopen.returns(res) addresses = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'NJ'), - ]), - ] + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + }, + { + "address": "8 Wildwood Drive", + "city": "Old Lyme", + "state": "NJ", + }, + ] res = verify('foo_id', addresses) # eq does not work with exceptions. Process each item manually. eq(len(res), 2) eq( res[0], - OrderedDict([ - ('address', '6406 IVY LN'), - ('city', 'GREENBELT'), - ('state', 'MD'), - ('zip5', '20770'), - ('zip4', '1441'), - ]), + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + }, ) expected = USPSError("-2147219400", "Invalid City.") eq(res[1], expected) @@ -411,11 +447,13 @@ def test_verify_api_empty_error(fake_urlopen): """) fake_urlopen.returns(res) - address = [OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'NJ'), - ])] + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "NJ", + } + ] err = assert_raises( TypeError, verify, @@ -436,17 +474,17 @@ def test_verify_api_order_error(fake_urlopen): fake_urlopen.returns(res) addresses = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'CT'), - ]), - ] + { + 'address': '6406 Ivy Lane', + 'city': 'Greenbelt', + 'state': 'MD', + }, + { + 'address': '8 Wildwood Drive', + 'city': 'Old Lyme', + 'state': 'CT', + } + ] err = assert_raises( IndexError, verify, From a4f3da09d2da8f91dd54293c69d71857fba4d114 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 15:21:51 -0600 Subject: [PATCH 11/21] Support empty inputs Before we raised an error when the USPS API freacked out about the bad XML, but really this should be fine, just return an empty list This also adds a test for the previous change, where we support iterables of inputs, not just sized ones. --- pyusps/address_information.py | 22 +++++++---- pyusps/test/test_address_information.py | 50 +++++++++++++++---------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/pyusps/address_information.py b/pyusps/address_information.py index c02551d..322334e 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -97,18 +97,23 @@ def _get_response(xml): res = etree.parse(res) return res +def _convert_input(input: Iterable[dict]) -> list[dict]: + result = [] + for i, address in enumerate(input): + if i >= ADDRESS_MAX: + # Raise here. The Verify API will not return an error. It will + # just return the first 5 results + raise ValueError(f'Only {ADDRESS_MAX} addresses are allowed per request') + result.append(address) + return result + def _create_xml( user_id, - addresses: Iterable, + addresses: list[dict], ): root = etree.Element('AddressValidateRequest', USERID=user_id) for i, arg in enumerate(addresses): - if i >= ADDRESS_MAX: - # Raise here. The Verify API will not return an error. It will - # just return the first 5 results - raise ValueError(f'Only {ADDRESS_MAX} addresses are allowed per request') - address = arg['address'] city = arg['city'] state = arg.get('state', None) @@ -170,7 +175,10 @@ def _create_xml( return root -def verify(user_id: str, addresses: Iterable) -> "list[dict]": +def verify(user_id: str, addresses: "Iterable[dict]") -> "list[dict]": + addresses = _convert_input(addresses) + if len(addresses) == 0: + return [] xml = _create_xml(user_id, addresses) res = _get_response(xml) res = _parse_response(res) diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index a6b66e9..d1340e2 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -276,6 +276,7 @@ def test_verify_urbanization(fake_urlopen): ] eq(res, expected) + @fudge.patch('pyusps.urlutil.urlopen') def test_verify_multiple(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() @@ -284,8 +285,7 @@ def test_verify_multiple(fake_urlopen): res = StringIO(u"""
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""") fake_urlopen.returns(res) - - addresses = [ + addresses_list = [ { "address": "6406 Ivy Lane", "city": "Greenbelt", @@ -297,25 +297,35 @@ def test_verify_multiple(fake_urlopen): "state": "CT", }, ] - res = verify('foo_id', addresses) - - expected = [ - { - "address": "6406 IVY LN", - "city": "GREENBELT", - "state": "MD", - "zip5": "20770", - "zip4": "1441", + addresses_generator = (a for a in addresses_list) + for inp in (addresses_list, addresses_generator): + res = verify('foo_id', inp) + + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + }, + { + "address": "8 WILDWOOD DR", + "city": "OLD LYME", + "state": "CT", + "zip5": "06371", + "zip4": "1844", }, - { - "address": "8 WILDWOOD DR", - "city": "OLD LYME", - "state": "CT", - "zip5": "06371", - "zip4": "1844", - }, - ] - eq(res, expected) + ] + eq(res, expected) + + +def test_empty_input(): + """We can handle empty input.""" + result = verify("user_id", []) + expected = [] + eq(result, expected) + @fudge.patch('pyusps.urlutil.urlopen') def test_verify_more_than_5(fake_urlopen): From 98e694792ee964bf2ed263e832d44c3baf17e5c9 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 15:36:54 -0600 Subject: [PATCH 12/21] Use RuntimeError, not IndexError or TypeError These old error types really don't make sense per their intended use described at https://docs.python.org/3/library/exceptions.html#bltin-exceptions Also update the README to be up to date with previous changes. --- README.rst | 36 +++++++++++++++---------- pyusps/address_information.py | 6 ++--- pyusps/test/test_address_information.py | 8 +++--- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index db20f18..f3b77ff 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,8 @@ function. Requests -------- -It takes in the user ID given to you by the USPS and a list of addresses to verify. +It takes in the user ID given to you by the USPS and an iterable of addresses to verify. +You can only supply up to 5 addresses at a time due to the API's limits. Each address is a dict containing the following required keys: :address: The street address @@ -49,7 +50,7 @@ The following keys are optional: Responses --------- -The response will either be list of dicts. Each address +The response will either be list of `dict`s or `USPSError`s. Each `dict` will always contain the following keys: :address: The street address @@ -59,7 +60,7 @@ will always contain the following keys: :zip4: The last four numbers of the zip code -Each address can optionally contain the following keys: +Each `dict` can optionally contain the following keys: :firm_name: The company name, e.g., XYZ Corp. :address_extended: An apartment, suite number, etc @@ -69,27 +70,29 @@ Each address can optionally contain the following keys: *firm_name, address_extended and urbanization will return the value requested if the API does not find a match.* -For multiple addresses, the order in which the addresses +If the USPS can't find an address, then in the response list, instead of a `dict` you +will receive a `USPSError`. `USPSError` is a subclass of `RuntimeError`, and has the +additional attributes of `code` and `description` for the error. + +The order in which the addresses were specified in the request is preserved in the response. Errors ------ -A ValueError will be raised if there's a general error, e.g., -invalid user id, or if a single address request generates an error. -Except for a general error, multiple addresses requests do not raise errors. -Instead, if one of the addresses generates an error, the -ValueError object is returned along with the rest of the results. - +- ValueError will be raised if you request more than 5 addresses. +- RuntimeError will be raised when the API returns a response that we can't parse + or otherwise doesn't make sense (You shouldn't run into this). +- A USPSError will be raised if the supplied user_id is invalid. Example ------- -Mutiple addresses error:: +Mutiple addresses, one of them isn't found so an error is returned:: - from pyusps import address_information + >>> from pyusps import address_information - addrs = [ + >>> addrs = [ { "address": "6406 Ivy Lane", "city": "Greenbelt", @@ -101,7 +104,8 @@ Mutiple addresses error:: "state": "NJ", }, ] - address_information.verify('foo_id', addrs) + >>> results = address_information.verify('foo_id', addrs) + >>> results [ { 'address': '6406 IVY LN', @@ -115,6 +119,10 @@ Mutiple addresses error:: }, USPSError('-2147219400: Invalid City. '), ] + >>> results[1].code + '-2147219400' + >>> res[1].description + 'Invalid City. ' Reference diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 322334e..36e1bed 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -70,7 +70,7 @@ def _process_multiple(addresses): msg = ('The addresses returned are not in the same ' 'order they were requested' ) - raise IndexError(msg) + raise RuntimeError(msg) result = _parse_address(address) results.append(result) @@ -84,9 +84,7 @@ def _parse_response(res): results = res.findall('Address') if len(results) == 0: - raise TypeError( - 'Could not find any address or error information' - ) + raise RuntimeError('Could not find any address or error information') return _process_multiple(results) def _get_response(xml): diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index d1340e2..2281f17 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -465,13 +465,13 @@ def test_verify_api_empty_error(fake_urlopen): } ] err = assert_raises( - TypeError, + RuntimeError, verify, 'foo_id', address ) - expected = TypeError('Could not find any address or error information') + expected = RuntimeError('Could not find any address or error information') assert_errors_equal(err, expected) @fudge.patch('pyusps.urlutil.urlopen') @@ -496,12 +496,12 @@ def test_verify_api_order_error(fake_urlopen): } ] err = assert_raises( - IndexError, + RuntimeError, verify, 'foo_id', addresses ) - expected = IndexError( + expected = RuntimeError( 'The addresses returned are not in the same order they were requested' ) assert_errors_equal(err, expected) From ce62930c59633a498e4286ed8caff5ebbb9b27c9 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 15:54:08 -0600 Subject: [PATCH 13/21] Strip whitespace from USPS error messages IDK why it includes that, but it does. --- README.rst | 4 ++-- pyusps/address_information.py | 4 ++-- pyusps/test/test_address_information.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index f3b77ff..26ddd91 100644 --- a/README.rst +++ b/README.rst @@ -117,12 +117,12 @@ Mutiple addresses, one of them isn't found so an error is returned:: 'zip4': '1435', 'zip5': '20770' }, - USPSError('-2147219400: Invalid City. '), + USPSError('-2147219400: Invalid City.'), ] >>> results[1].code '-2147219400' >>> res[1].description - 'Invalid City. ' + 'Invalid City.' Reference diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 36e1bed..32aa4c6 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -30,8 +30,8 @@ def __eq__(self, o: object) -> bool: def _get_error(node): if node.tag != 'Error': return None - code = node.find('Number').text - description = node.find('Description').text + code = node.find('Number').text.strip() + description = node.find('Description').text.strip() return USPSError(code, description) def _get_address_error(address): diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 2281f17..14bbebb 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -416,7 +416,7 @@ def test_verify_api_address_error_multiple(fake_urlopen): req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" fake_urlopen = fake_urlopen.with_args(req) res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
-2147219400API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllInvalid City.1000440
""") +
6406 IVY LNGREENBELTMD207701441
-2147219400API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllInvalid City. 1000440
""") fake_urlopen.returns(res) addresses = [ From 5cc9166bee23c365df9098a565b5bce5035aa51e Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 16:07:57 -0600 Subject: [PATCH 14/21] Add CHANGELOG.md --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2c64cd0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# CHANGELOG + +## 1.0.0 + +See https://github.com/thelinuxkid/pyusps/issues/10 for +the motivation of many of these changes. + +### Changed + +- Drop support for anythin below Python 3.6 +- Main signature changes from `verify(user_id, *args)` to `verify(user_id, args)` + You must always supply an iterable of inputs. Single inputs are no longer supported, + just wrap them in a length-one list if you need. Always returns a list of + `dicts`/`USPSError`s. +- Instead of `OrderedDict`s, we just return plain old `dict`s. +- Changed returned/raised `ValueError`s from not-found addresses to be always-returned + `USPSError`s +- Instead of `TypeError` or `ValueError` from parsing errors, we now consistently raise + `RuntimeError`s +- Removed `api_url` from global namespace. IDK why you would have relied on this though. + +### Added + +- Supports supplying an iterable of addresses, no longer needs the __len__ method. +- If you supply an empty iterable as input, you get back an empty list, not an error. +- The new `USPSError` includes the attributes `code: str` and `description: str` + so you can get the original error without having to do string parsing. +- Testing on GitHub Actions! \ No newline at end of file From a3acffcaa59963311295e3fbe03fb9098920acd5 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 16:20:44 -0600 Subject: [PATCH 15/21] Support inputs that are Mappings, not just dicts Before each input had to have the `get()` method, but now it only needs the `__getitem__()` method. --- CHANGELOG.md | 2 ++ README.rst | 5 ++--- pyusps/address_information.py | 36 ++++++++++++++++++++++------------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c64cd0..baeb0a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ the motivation of many of these changes. ### Added - Supports supplying an iterable of addresses, no longer needs the __len__ method. +- Each supplied input now only needs to have a `__getitem__()` method. Before, it + needed that but also a `get()` method. - If you supply an empty iterable as input, you get back an empty list, not an error. - The new `USPSError` includes the attributes `code: str` and `description: str` so you can get the original error without having to do string parsing. diff --git a/README.rst b/README.rst index 26ddd91..e1446aa 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,8 @@ Requests It takes in the user ID given to you by the USPS and an iterable of addresses to verify. You can only supply up to 5 addresses at a time due to the API's limits. -Each address is a dict containing the following required keys: +Each address is a dict-like (e.g. supports `__getitem__()`) containing the following +required keys: :address: The street address :city: The city @@ -45,8 +46,6 @@ The following keys are optional: :address_extended: An apartment, suite number, etc :urbanization: For Puerto Rico addresses only - - Responses --------- diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 32aa4c6..cf50796 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -1,4 +1,5 @@ -from typing import Iterable +from collections.abc import Mapping +from typing import Any, Iterable, Union from lxml import etree @@ -27,6 +28,8 @@ def __eq__(self, o: object) -> bool: self.description == o.description ) +Result = Union[dict, USPSError] + def _get_error(node): if node.tag != 'Error': return None @@ -41,7 +44,7 @@ def _get_address_error(address): else: return _get_error(error_node) -def _parse_address(address): +def _parse_address(address) -> dict: result = {} # More user-friendly names for street # attributes @@ -57,7 +60,7 @@ def _parse_address(address): result[name] = child.text return result -def _process_multiple(addresses): +def _process_multiple(addresses) -> "list[Result]": results = [] for i, address in enumerate(addresses): # Return error object if there are @@ -76,7 +79,7 @@ def _process_multiple(addresses): return results -def _parse_response(res): +def _parse_response(res) -> "list[Result]": # General error, e.g., authorization error = _get_error(res.getroot()) if error is not None: @@ -95,7 +98,7 @@ def _get_response(xml): res = etree.parse(res) return res -def _convert_input(input: Iterable[dict]) -> list[dict]: +def _convert_input(input: Iterable[Mapping]) -> list[Mapping]: result = [] for i, address in enumerate(input): if i >= ADDRESS_MAX: @@ -105,20 +108,27 @@ def _convert_input(input: Iterable[dict]) -> list[dict]: result.append(address) return result +def _get(mapping: Mapping, key: str) -> Any: + """Wrapper so that mapping only has to implement __getitem__, not get().""" + try: + return mapping[key] + except KeyError: + return None + def _create_xml( - user_id, - addresses: list[dict], + user_id: str, + addresses: "list[Mapping]", ): root = etree.Element('AddressValidateRequest', USERID=user_id) for i, arg in enumerate(addresses): address = arg['address'] city = arg['city'] - state = arg.get('state', None) - zip_code = arg.get('zip_code', None) - address_extended = arg.get('address_extended', None) - firm_name = arg.get('firm_name', None) - urbanization = arg.get('urbanization', None) + state = _get(arg, 'state') + zip_code = _get(arg, 'zip_code') + address_extended = _get(arg, 'address_extended') + firm_name = _get(arg, 'firm_name') + urbanization = _get(arg, 'urbanization') address_el = etree.Element('Address', ID=str(i)) root.append(address_el) @@ -173,7 +183,7 @@ def _create_xml( return root -def verify(user_id: str, addresses: "Iterable[dict]") -> "list[dict]": +def verify(user_id: str, addresses: "Iterable[Mapping]") -> "list[Union[dict, USPSError]]": addresses = _convert_input(addresses) if len(addresses) == 0: return [] From a15beceac9f6b0b7d68e3700261bce518d04e5fa Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 16:30:18 -0600 Subject: [PATCH 16/21] Remove python2 compat layer for URLs --- pyusps/address_information.py | 7 +++--- pyusps/test/test_address_information.py | 32 ++++++++++++------------- pyusps/urlutil.py | 17 ------------- 3 files changed, 20 insertions(+), 36 deletions(-) delete mode 100644 pyusps/urlutil.py diff --git a/pyusps/address_information.py b/pyusps/address_information.py index cf50796..8a79fdc 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -3,7 +3,8 @@ from lxml import etree -import pyusps.urlutil +from urllib.parse import urlencode +from urllib.request import urlopen ADDRESS_MAX = 5 @@ -92,9 +93,9 @@ def _parse_response(res) -> "list[Result]": def _get_response(xml): params = {'API': 'Verify', 'XML': etree.tostring(xml)} - param_string = pyusps.urlutil.urlencode(params) + param_string = urlencode(params) url = f'https://production.shippingapis.com/ShippingAPI.dll?{param_string}' - res = pyusps.urlutil.urlopen(url) + res = urlopen(url) res = etree.parse(res) return res diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 14bbebb..284556a 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -6,7 +6,7 @@ from pyusps.address_information import verify, USPSError from pyusps.test.util import assert_raises, assert_errors_equal -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_simple(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -36,7 +36,7 @@ def test_verify_simple(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_zip5(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -66,7 +66,7 @@ def test_verify_zip5(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_both(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E1441%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -96,7 +96,7 @@ def test_verify_zip_both(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_dash(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E1441%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -126,7 +126,7 @@ def test_verify_zip_dash(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_only(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%2F%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -155,7 +155,7 @@ def test_verify_zip_only(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_state_only(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -184,7 +184,7 @@ def test_verify_state_only(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_firm_name(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CFirmName%3EXYZ+Corp%3C%2FFirmName%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -215,7 +215,7 @@ def test_verify_firm_name(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_address_extended(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%3ESuite+12%3C%2FAddress1%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -245,7 +245,7 @@ def test_verify_address_extended(fake_urlopen): ] eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_urbanization(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CUrbanization%3EPuerto+Rico%3C%2FUrbanization%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -277,7 +277,7 @@ def test_verify_urbanization(fake_urlopen): eq(res, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_multiple(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ECT%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -327,7 +327,7 @@ def test_empty_input(): eq(result, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_more_than_5(fake_urlopen): addr = { "address": "6406 Ivy Lane", @@ -358,7 +358,7 @@ def test_error_properties(): eq(err, err2) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_api_root_error(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -388,7 +388,7 @@ def test_verify_api_root_error(fake_urlopen): ) eq(err, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_api_address_error_single(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -410,7 +410,7 @@ def test_verify_api_address_error_single(fake_urlopen): expected = USPSError("-2147219401", "Address Not Found.") eq(res[0], expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_api_address_error_multiple(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -448,7 +448,7 @@ def test_verify_api_address_error_multiple(fake_urlopen): expected = USPSError("-2147219400", "Invalid City.") eq(res[1], expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_api_empty_error(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" @@ -474,7 +474,7 @@ def test_verify_api_empty_error(fake_urlopen): expected = RuntimeError('Could not find any address or error information') assert_errors_equal(err, expected) -@fudge.patch('pyusps.urlutil.urlopen') +@fudge.patch('pyusps.address_information.urlopen') def test_verify_api_order_error(fake_urlopen): fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ECT%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" diff --git a/pyusps/urlutil.py b/pyusps/urlutil.py deleted file mode 100644 index 5feae2e..0000000 --- a/pyusps/urlutil.py +++ /dev/null @@ -1,17 +0,0 @@ -# `fudge.patch` in `pyusps.test.test_address_information` needs the full -# module path as well as the function name as its argument, e.g., -# "urllib2.urlopen". Create a normalized module path here for -# urllib2/urllib functions in order to support both Python 2 and Python 3. - -try: - from urllib.request import urlopen as _urlopen -except ImportError: - from urllib2 import urlopen as _urlopen - -try: - from urllib.parse import urlencode as _urlencode -except ImportError: - from urllib import urlencode as _urlencode - -urlopen = _urlopen -urlencode = _urlencode From 58b27c06596d4f082a0ab50273e781a48fd54db4 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 16:42:15 -0600 Subject: [PATCH 17/21] Factor out mocking boilerplate --- pyusps/test/test_address_information.py | 112 ++++++++---------------- 1 file changed, 37 insertions(+), 75 deletions(-) diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 284556a..9739a9a 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -6,14 +6,16 @@ from pyusps.address_information import verify, USPSError from pyusps.test.util import assert_raises, assert_errors_equal +def setup_urlopen_mock(mock, expects, return_str): + mock = mock.expects_call().with_args(expects) + mock = mock.returns(StringIO(return_str)) + return mock + @fudge.patch('pyusps.address_information.urlopen') def test_verify_simple(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -38,12 +40,9 @@ def test_verify_simple(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_zip5(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -68,12 +67,9 @@ def test_verify_zip5(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_both(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E1441%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -98,12 +94,9 @@ def test_verify_zip_both(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_dash(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E1441%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -128,12 +121,9 @@ def test_verify_zip_dash(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_zip_only(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%2F%3E%3CZip5%3E20770%3C%2FZip5%3E%3CZip4%3E%3C%2FZip4%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -157,12 +147,9 @@ def test_verify_zip_only(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_state_only(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -186,12 +173,9 @@ def test_verify_state_only(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_firm_name(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CFirmName%3EXYZ+Corp%3C%2FFirmName%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
XYZ CORP6406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
XYZ CORP6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -217,12 +201,9 @@ def test_verify_firm_name(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_address_extended(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%3ESuite+12%3C%2FAddress1%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
STE 126406 IVY LNGREENBELTMD207701441
""") - fake_urlopen.returns(res) + res = u"""
STE 126406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -247,12 +228,9 @@ def test_verify_address_extended(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_urbanization(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CUrbanization%3EPuerto+Rico%3C%2FUrbanization%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -279,12 +257,10 @@ def test_verify_urbanization(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_multiple(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ECT%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""" + setup_urlopen_mock(fake_urlopen, req, res) + addresses_list = [ { "address": "6406 Ivy Lane", @@ -360,15 +336,13 @@ def test_error_properties(): @fudge.patch('pyusps.address_information.urlopen') def test_verify_api_root_error(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" + res = u""" 80040b1a Authorization failure. Perhaps username and/or password is incorrect. UspsCom::DoAuth -""") - fake_urlopen.returns(res) + """ + setup_urlopen_mock(fake_urlopen, req, res) address = { "address": "6406 Ivy Lane", @@ -390,12 +364,9 @@ def test_verify_api_root_error(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_api_address_error_single(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""") - fake_urlopen.returns(res) + res = u"""
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -412,12 +383,9 @@ def test_verify_api_address_error_single(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_api_address_error_multiple(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
-2147219400API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllInvalid City. 1000440
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
-2147219400API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllInvalid City. 1000440
""" + setup_urlopen_mock(fake_urlopen, req, res) addresses = [ { @@ -450,12 +418,9 @@ def test_verify_api_address_error_multiple(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_api_empty_error(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3ENJ%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -""") - fake_urlopen.returns(res) + res = u"""""" + setup_urlopen_mock(fake_urlopen, req, res) address = [ { @@ -476,12 +441,9 @@ def test_verify_api_empty_error(fake_urlopen): @fudge.patch('pyusps.address_information.urlopen') def test_verify_api_order_error(fake_urlopen): - fake_urlopen = fake_urlopen.expects_call() req = """https://production.shippingapis.com/ShippingAPI.dll?API=Verify&XML=%3CAddressValidateRequest+USERID%3D%22foo_id%22%3E%3CAddress+ID%3D%220%22%3E%3CAddress1%2F%3E%3CAddress2%3E6406+Ivy+Lane%3C%2FAddress2%3E%3CCity%3EGreenbelt%3C%2FCity%3E%3CState%3EMD%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3CAddress+ID%3D%221%22%3E%3CAddress1%2F%3E%3CAddress2%3E8+Wildwood+Drive%3C%2FAddress2%3E%3CCity%3EOld+Lyme%3C%2FCity%3E%3CState%3ECT%3C%2FState%3E%3CZip5%2F%3E%3CZip4%2F%3E%3C%2FAddress%3E%3C%2FAddressValidateRequest%3E""" - fake_urlopen = fake_urlopen.with_args(req) - res = StringIO(u""" -
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""") - fake_urlopen.returns(res) + res = u"""
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""" + setup_urlopen_mock(fake_urlopen, req, res) addresses = [ { From a2333d8085d185b9eaa84496f18ccebcb4044396 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 16:46:27 -0600 Subject: [PATCH 18/21] Refactor a test --- pyusps/test/test_address_information.py | 35 +++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 9739a9a..86eb0c5 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -274,25 +274,26 @@ def test_verify_multiple(fake_urlopen): }, ] addresses_generator = (a for a in addresses_list) - for inp in (addresses_list, addresses_generator): - res = verify('foo_id', inp) - expected = [ - { - "address": "6406 IVY LN", - "city": "GREENBELT", - "state": "MD", - "zip5": "20770", - "zip4": "1441", - }, - { - "address": "8 WILDWOOD DR", - "city": "OLD LYME", - "state": "CT", - "zip5": "06371", - "zip4": "1844", + expected = [ + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", }, - ] + { + "address": "8 WILDWOOD DR", + "city": "OLD LYME", + "state": "CT", + "zip5": "06371", + "zip4": "1844", + }, + ] + + for inp in (addresses_list, addresses_generator): + res = verify('foo_id', inp) eq(res, expected) From 76672ddd8a4595675d809a25fce3699fad8f634c Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 17:17:31 -0600 Subject: [PATCH 19/21] Finish type hints, add mypy test --- .github/workflows/test.yaml | 13 ++++++++- CHANGELOG.md | 3 ++- pyusps/address_information.py | 35 +++++++++++-------------- pyusps/test/test_address_information.py | 2 ++ setup.py | 2 ++ 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 63e87fa..b6b337f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,18 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install dependencies and this library run: pip install -e .[test] - name: Run tests run: nosetests + lint: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: "3.9.6" # Latest python that still works with fudge + - name: Install dependencies and this library + run: pip install -e .[test] + - name: Run MyPy + run: mypy pyusps diff --git a/CHANGELOG.md b/CHANGELOG.md index baeb0a6..a079595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,4 +27,5 @@ the motivation of many of these changes. - If you supply an empty iterable as input, you get back an empty list, not an error. - The new `USPSError` includes the attributes `code: str` and `description: str` so you can get the original error without having to do string parsing. -- Testing on GitHub Actions! \ No newline at end of file +- Testing on GitHub Actions! +- Type hints \ No newline at end of file diff --git a/pyusps/address_information.py b/pyusps/address_information.py index 8a79fdc..ac2eb16 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -38,14 +38,14 @@ def _get_error(node): description = node.find('Description').text.strip() return USPSError(code, description) -def _get_address_error(address): +def _get_address_error(address: etree._Element) -> Union[USPSError, None]: error_node = address.find('Error') if error_node is None: return None else: return _get_error(error_node) -def _parse_address(address) -> dict: +def _parse_address(address: etree._Element) -> dict: result = {} # More user-friendly names for street # attributes @@ -61,11 +61,12 @@ def _parse_address(address) -> dict: result[name] = child.text return result -def _process_multiple(addresses) -> "list[Result]": +def _process_multiple(addresses: "list[etree._Element]") -> "list[Result]": results = [] for i, address in enumerate(addresses): # Return error object if there are # multiple items + result: Result error = _get_address_error(address) if error is not None: result = error @@ -80,26 +81,26 @@ def _process_multiple(addresses) -> "list[Result]": return results -def _parse_response(res) -> "list[Result]": +def _parse_response(res: etree._ElementTree) -> "list[Result]": # General error, e.g., authorization error = _get_error(res.getroot()) if error is not None: raise error - results = res.findall('Address') - if len(results) == 0: + elements = res.findall('Address') + if len(elements) == 0: raise RuntimeError('Could not find any address or error information') - return _process_multiple(results) + return _process_multiple(elements) -def _get_response(xml): +def _get_response(xml: etree._Element) -> etree._ElementTree: params = {'API': 'Verify', 'XML': etree.tostring(xml)} param_string = urlencode(params) url = f'https://production.shippingapis.com/ShippingAPI.dll?{param_string}' res = urlopen(url) - res = etree.parse(res) - return res + tree = etree.parse(res) + return tree -def _convert_input(input: Iterable[Mapping]) -> list[Mapping]: +def _convert_input(input: "Iterable[Mapping]") -> "list[Mapping]": result = [] for i, address in enumerate(input): if i >= ADDRESS_MAX: @@ -116,10 +117,7 @@ def _get(mapping: Mapping, key: str) -> Any: except KeyError: return None -def _create_xml( - user_id: str, - addresses: "list[Mapping]", - ): +def _create_xml(user_id: str, addresses: "list[Mapping]") -> etree._Element: root = etree.Element('AddressValidateRequest', USERID=user_id) for i, arg in enumerate(addresses): @@ -188,8 +186,7 @@ def verify(user_id: str, addresses: "Iterable[Mapping]") -> "list[Union[dict, US addresses = _convert_input(addresses) if len(addresses) == 0: return [] - xml = _create_xml(user_id, addresses) - res = _get_response(xml) - res = _parse_response(res) - + xml_request = _create_xml(user_id, addresses) + xml_response = _get_response(xml_request) + res = _parse_response(xml_response) return res diff --git a/pyusps/test/test_address_information.py b/pyusps/test/test_address_information.py index 86eb0c5..132c5d0 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -1,3 +1,5 @@ +# type: ignore + import fudge from nose.tools import eq_ as eq diff --git a/setup.py b/setup.py index e7fbdc5..9f3e938 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,8 @@ EXTRAS_REQUIRES = dict( test=[ 'fudge>=1.1.1', + 'lxml_stubs>=0.4.0', + 'mypy>=0.942', 'nose>=1.3.7', ], dev=[ From d9e51679841782b05f47a6d4486c2c2d010f9ba3 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 18:06:47 -0600 Subject: [PATCH 20/21] Move to static metadata It is recommended to use sttic metadata if possible per https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata --- MANIFEST.in | 2 -- pyproject.toml | 3 +++ setup.cfg | 38 +++++++++++++++++++++++++++++++++++ setup.py | 54 +++----------------------------------------------- 4 files changed, 44 insertions(+), 53 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index a5021c6..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include README.rst -include LICENSE diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9babed1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..97aa907 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,38 @@ +[metadata] +name = pyusps +version = 0.0.7 +author = Andres Buritica +author_email = andres@thelinuxkid.com +description = pyusps -- Python bindings for the USPS Ecommerce APIs +license = MIT +license_file = LICENSE +long_description = file: README.rst +long_description_content_type = text/x-rst +url = https://github.com/thelinuxkid/pyusps +project_urls = + Tracker = https://github.com/thelinuxkid/pyusps/issues + Source = https://github.com/thelinuxkid/pyusps +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + Natural Language :: English + License :: OSI Approved :: MIT License + Programming Language :: Python + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.6 +install_requires = + lxml>=2.3.3 + +[options.extras_require] +test = + fudge>=1.1.1 + lxml_stubs>=0.4.0 + mypy>=0.942 + nose>=1.3.7 +dev = + ipython>=5.5.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 9f3e938..f13cf21 100755 --- a/setup.py +++ b/setup.py @@ -1,51 +1,3 @@ -#!/usr/bin/python -from setuptools import setup, find_packages -import os - -EXTRAS_REQUIRES = dict( - test=[ - 'fudge>=1.1.1', - 'lxml_stubs>=0.4.0', - 'mypy>=0.942', - 'nose>=1.3.7', - ], - dev=[ - 'ipython>=5.5.0', - ], - ) - -# Pypi package documentation -root = os.path.dirname(__file__) -path = os.path.join(root, 'README.rst') -with open(path) as fp: - long_description = fp.read() - -setup( - name='pyusps', - version='0.0.7', - description='pyusps -- Python bindings for the USPS Ecommerce APIs', - long_description=long_description, - author='Andres Buritica', - author_email='andres@thelinuxkid.com', - maintainer='Andres Buritica', - maintainer_email='andres@thelinuxkid.com', - url='https://github.com/thelinuxkid/pyusps', - license='MIT', - packages = find_packages(), - namespace_packages = ['pyusps'], - test_suite='nose.collector', - install_requires=[ - 'setuptools>=0.6c11', - 'lxml>=2.3.3', - ], - extras_require=EXTRAS_REQUIRES, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5' - ], -) +from setuptools import setup +if __name__ == '__main__': + setup() From b3d87e7021656316d200231e278b4fb4fdfb931b Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Tue, 5 Apr 2022 18:16:51 -0600 Subject: [PATCH 21/21] Automate build and deploy --- .github/workflows/test.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b6b337f..ef87bfd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -38,3 +38,27 @@ jobs: run: pip install -e .[test] - name: Run MyPy run: mypy pyusps + build_dist: + name: Build wheels + needs: [test, lint] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Build sdist and wheel + run: | + pip install build + python -m build + - uses: actions/upload-artifact@v2 + with: + name: dist + path: dist/ + - name: Publish wheels to PyPI + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + pip install twine + twine upload dist/* + continue-on-error: true