├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── pyusps ├── __init__.py ├── address_information.py ├── test │ ├── __init__.py │ ├── test_address_information.py │ └── util.py └── urlutil.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | #Virtualenv 30 | .virtual 31 | 32 | #Ropemacs 33 | .ropeproject 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Andres Buritica 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | pyusps 3 | ====== 4 | 5 | Description 6 | =========== 7 | 8 | pyusps is a pythonic wrapper for the USPS Ecommerce APIs. 9 | Currently, only the Address Information API is supported. 10 | 11 | Installation 12 | ============ 13 | 14 | Install using pip:: 15 | 16 | pip install pyusps 17 | 18 | or easy_install:: 19 | 20 | easy_install pyusps 21 | 22 | Address Information API 23 | ======================= 24 | 25 | This API is avaiable via the pyusps.address_information.verify 26 | function. It takes in the user ID given to you by the USPS 27 | and a variable length list of addresses to verify. 28 | 29 | Requests 30 | -------- 31 | 32 | Each address is a dict containing the following required keys: 33 | 34 | :address: The street address 35 | :city: The city 36 | :state: The state 37 | :zip_code: The zip code in one the following formats: xxxxx, xxxxx-xxxx, or xxxxxxxxx 38 | 39 | *Only one of state or zip_code is needed.* 40 | 41 | The following keys are optional: 42 | 43 | :firm_name: The company name, e.g., XYZ Corp. Although the API documentation says this field is required, tests show that it isn't. 44 | :address_extended: An apartment, suite number, etc 45 | :urbanization: For Puerto Rico addresses only 46 | 47 | 48 | 49 | Responses 50 | --------- 51 | 52 | The response will either be a dict, if a single address was requested, 53 | or a list of dicts, if multiple addresses were requested. Each address 54 | will always contain the following keys: 55 | 56 | :address: The street address 57 | :city: The city 58 | :state: The state 59 | :zip5: The first five numbers of the zip code 60 | :zip4: The last four numbers of the zip code 61 | 62 | 63 | Each address can optionally contain the following keys: 64 | 65 | :firm_name: The company name, e.g., XYZ Corp. 66 | :address_extended: An apartment, suite number, etc 67 | :urbanization: For Puerto Rico addresses only 68 | :returntext: Additional information about the address, usually a warning, e.g., "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." 69 | 70 | *firm_name, address_extended and urbanization will return the value 71 | requested if the API does not find a match.* 72 | 73 | For multiple addresses, the order in which the addresses 74 | were specified in the request is preserved in the response. 75 | 76 | Errors 77 | ------ 78 | 79 | A ValueError will be raised if there's a general error, e.g., 80 | invalid user id, or if a single address request generates an error. 81 | Except for a general error, multiple addresses requests do not raise errors. 82 | Instead, if one of the addresses generates an error, the 83 | ValueError object is returned along with the rest of the results. 84 | 85 | 86 | Examples 87 | -------- 88 | 89 | Single address request:: 90 | 91 | from pyusps import address_information 92 | 93 | addr = dict([ 94 | ('address', '6406 Ivy Lane'), 95 | ('city', 'Greenbelt'), 96 | ('state', 'MD'), 97 | ]) 98 | address_information.verify('foo_id', addr) 99 | dict([ 100 | ('address', '6406 IVY LN'), 101 | ('city', 'GREENBELT'), 102 | ('state', 'MD'), 103 | ('zip5', '20770'), 104 | ('zip4', '1441'), 105 | ]) 106 | 107 | Mutiple addresses request:: 108 | 109 | from pyusps import address_information 110 | 111 | addrs = [ 112 | dict([ 113 | ('address', '6406 Ivy Lane'), 114 | ('city', 'Greenbelt'), 115 | ('state', 'MD'), 116 | ]), 117 | dict([ 118 | ('address', '8 Wildwood Drive'), 119 | ('city', 'Old Lyme'), 120 | ('state', 'CT'), 121 | ]), 122 | ] 123 | address_information.verify('foo_id', *addrs) 124 | [ 125 | dict([ 126 | ('address', '6406 IVY LN'), 127 | ('city', 'GREENBELT'), 128 | ('state', 'MD'), 129 | ('zip5', '20770'), 130 | ('zip4', '1441'), 131 | ]), 132 | dict([ 133 | ('address', '8 WILDWOOD DR'), 134 | ('city', 'OLD LYME'), 135 | ('state', 'CT'), 136 | ('zip5', '06371'), 137 | ('zip4', '1844'), 138 | ]), 139 | ] 140 | 141 | Mutiple addresses error:: 142 | 143 | from pyusps import address_information 144 | 145 | addrs = [ 146 | dict([ 147 | ('address', '6406 Ivy Lane'), 148 | ('city', 'Greenbelt'), 149 | ('state', 'MD'), 150 | ]), 151 | dict([ 152 | ('address', '8 Wildwood Drive'), 153 | ('city', 'Old Lyme'), 154 | ('state', 'NJ'), 155 | ]), 156 | ] 157 | address_information.verify('foo_id', *addrs) 158 | [ 159 | dict([ 160 | ('address', '6406 IVY LN'), 161 | ('city', 'GREENBELT'), 162 | ('state', 'MD'), 163 | ('zip5', '20770'), 164 | ('zip4', '1441'), 165 | ]), 166 | ValueError('-2147219400: Invalid City. '), 167 | ] 168 | 169 | Reference 170 | --------- 171 | For more information on the Address Information API visit https://www.usps.com/business/web-tools-apis/address-information-api.htm 172 | 173 | Developing 174 | ========== 175 | 176 | External dependencies 177 | --------------------- 178 | 179 | - libxml2-dev 180 | - libxslt1-dev 181 | - build-essential 182 | - python-dev or python3-dev 183 | - python-setuptools or python3-setuptools 184 | - virtualenvwrapper 185 | 186 | Setup 187 | ----- 188 | 189 | To start developing, run the following commands from the project's base 190 | directory. You can download the source from 191 | https://github.com/thelinuxkid/pyusps:: 192 | 193 | mkvirtualenv pyusps 194 | python setup.py develop 195 | # At this point, pyusps will already be in easy-install.pth. 196 | # So, pip will not attempt to download it 197 | pip install pyusps[test] 198 | 199 | If you like to use ipython you can install it with the dev 200 | requirement:: 201 | 202 | pip install pyusps[dev] 203 | 204 | Testing 205 | ------- 206 | 207 | To run the unit-tests run the following command from the project's 208 | base directory:: 209 | 210 | nosetests 211 | -------------------------------------------------------------------------------- /pyusps/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /pyusps/address_information.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from lxml import etree 4 | 5 | import pyusps.urlutil 6 | 7 | 8 | api_url = 'https://production.shippingapis.com/ShippingAPI.dll' 9 | address_max = 5 10 | 11 | def _find_error(root): 12 | if root.tag == 'Error': 13 | num = root.find('Number') 14 | desc = root.find('Description') 15 | return (num, desc) 16 | 17 | def _get_error(error): 18 | (num, desc) = error 19 | return ValueError( 20 | '{num}: {desc}'.format( 21 | num=num.text, 22 | desc=desc.text, 23 | ) 24 | ) 25 | 26 | def _get_address_error(address): 27 | error = address.find('Error') 28 | if error is not None: 29 | error = _find_error(error) 30 | return _get_error(error) 31 | 32 | def _parse_address(address): 33 | result = OrderedDict() 34 | for child in address.iterchildren(): 35 | # elements are yielded in order 36 | name = child.tag.lower() 37 | # More user-friendly names for street 38 | # attributes 39 | if name == 'address2': 40 | name = 'address' 41 | elif name == 'address1': 42 | name = 'address_extended' 43 | elif name == 'firmname': 44 | name = 'firm_name' 45 | result[name] = child.text 46 | 47 | return result 48 | 49 | def _process_one(address): 50 | # Raise address error if there's only one item 51 | error = _get_address_error(address) 52 | if error is not None: 53 | raise error 54 | 55 | return _parse_address(address) 56 | 57 | def _process_multiple(addresses): 58 | results = [] 59 | count = 0 60 | for address in addresses: 61 | # Return error object if there are 62 | # multiple items 63 | error = _get_address_error(address) 64 | if error is not None: 65 | result = error 66 | else: 67 | result = _parse_address(address) 68 | if str(count) != address.get('ID'): 69 | msg = ('The addresses returned are not in the same ' 70 | 'order they were requested' 71 | ) 72 | raise IndexError(msg) 73 | results.append(result) 74 | count += 1 75 | 76 | return results 77 | 78 | def _parse_response(res): 79 | # General error, e.g., authorization 80 | error = _find_error(res.getroot()) 81 | if error is not None: 82 | raise _get_error(error) 83 | 84 | results = res.findall('Address') 85 | length = len(results) 86 | if length == 0: 87 | raise TypeError( 88 | 'Could not find any address or error information' 89 | ) 90 | if length == 1: 91 | return _process_one(results.pop()) 92 | return _process_multiple(results) 93 | 94 | def _get_response(xml): 95 | params = OrderedDict([ 96 | ('API', 'Verify'), 97 | ('XML', etree.tostring(xml)), 98 | ]) 99 | url = '{api_url}?{params}'.format( 100 | api_url=api_url, 101 | params=pyusps.urlutil.urlencode(params), 102 | ) 103 | 104 | res = pyusps.urlutil.urlopen(url) 105 | res = etree.parse(res) 106 | 107 | return res 108 | 109 | def _create_xml( 110 | user_id, 111 | *args 112 | ): 113 | root = etree.Element('AddressValidateRequest', USERID=user_id) 114 | 115 | if len(args) > address_max: 116 | # Raise here. The Verify API will not return an error. It will 117 | # just return the first 5 results 118 | raise ValueError( 119 | 'Only {address_max} addresses are allowed per ' 120 | 'request'.format( 121 | address_max=address_max, 122 | ) 123 | ) 124 | 125 | for i,arg in enumerate(args): 126 | address = arg['address'] 127 | city = arg['city'] 128 | state = arg.get('state', None) 129 | zip_code = arg.get('zip_code', None) 130 | address_extended = arg.get('address_extended', None) 131 | firm_name = arg.get('firm_name', None) 132 | urbanization = arg.get('urbanization', None) 133 | 134 | address_el = etree.Element('Address', ID=str(i)) 135 | root.append(address_el) 136 | 137 | # Documentation says this tag is required but tests 138 | # show it isn't 139 | if firm_name is not None: 140 | firm_name_el = etree.Element('FirmName') 141 | firm_name_el.text = firm_name 142 | address_el.append(firm_name_el) 143 | 144 | address_1_el = etree.Element('Address1') 145 | if address_extended is not None: 146 | address_1_el.text = address_extended 147 | address_el.append(address_1_el) 148 | 149 | address_2_el = etree.Element('Address2') 150 | address_2_el.text = address 151 | address_el.append(address_2_el) 152 | 153 | city_el = etree.Element('City') 154 | city_el.text = city 155 | address_el.append(city_el) 156 | 157 | state_el = etree.Element('State') 158 | if state is not None: 159 | state_el.text = state 160 | address_el.append(state_el) 161 | 162 | if urbanization is not None: 163 | urbanization_el = etree.Element('Urbanization') 164 | urbanization_el.text = urbanization 165 | address_el.append(urbanization_el) 166 | 167 | zip5 = None 168 | zip4 = None 169 | if zip_code is not None: 170 | zip5 = zip_code[:5] 171 | zip4 = zip_code[5:] 172 | if zip4.startswith('-'): 173 | zip4 = zip4[1:] 174 | 175 | zip5_el = etree.Element('Zip5') 176 | if zip5 is not None: 177 | zip5_el.text = zip5 178 | address_el.append(zip5_el) 179 | 180 | zip4_el = etree.Element('Zip4') 181 | if zip4 is not None: 182 | zip4_el.text = zip4 183 | address_el.append(zip4_el) 184 | 185 | return root 186 | 187 | def verify(user_id, *args): 188 | xml = _create_xml(user_id, *args) 189 | res = _get_response(xml) 190 | res = _parse_response(res) 191 | 192 | return res 193 | -------------------------------------------------------------------------------- /pyusps/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thelinuxkid/pyusps/2ef98036a4d0a73fcf9d08ab4b5dbd6f825e5d7c/pyusps/test/__init__.py -------------------------------------------------------------------------------- /pyusps/test/test_address_information.py: -------------------------------------------------------------------------------- 1 | import fudge 2 | 3 | from collections import OrderedDict 4 | from nose.tools import eq_ as eq 5 | from io import StringIO 6 | 7 | from pyusps.address_information import verify 8 | from pyusps.test.util import assert_raises, assert_errors_equal 9 | 10 | @fudge.patch('pyusps.urlutil.urlopen') 11 | def test_verify_simple(fake_urlopen): 12 | fake_urlopen = fake_urlopen.expects_call() 13 | 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""" 14 | fake_urlopen = fake_urlopen.with_args(req) 15 | res = StringIO(u""" 16 |
6406 IVY LNGREENBELTMD207701441
""") 17 | fake_urlopen.returns(res) 18 | 19 | address = OrderedDict([ 20 | ('address', '6406 Ivy Lane'), 21 | ('city', 'Greenbelt'), 22 | ('state', 'MD'), 23 | ('zip_code', '20770'), 24 | ]) 25 | res = verify( 26 | 'foo_id', 27 | address, 28 | ) 29 | 30 | expected = OrderedDict([ 31 | ('address', '6406 IVY LN'), 32 | ('city', 'GREENBELT'), 33 | ('state', 'MD'), 34 | ('zip5', '20770'), 35 | ('zip4', '1441'), 36 | ]) 37 | eq(res, expected) 38 | 39 | @fudge.patch('pyusps.urlutil.urlopen') 40 | def test_verify_zip5(fake_urlopen): 41 | fake_urlopen = fake_urlopen.expects_call() 42 | 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""" 43 | fake_urlopen = fake_urlopen.with_args(req) 44 | res = StringIO(u""" 45 |
6406 IVY LNGREENBELTMD207701441
""") 46 | fake_urlopen.returns(res) 47 | 48 | address = OrderedDict([ 49 | ('address', '6406 Ivy Lane'), 50 | ('city', 'Greenbelt'), 51 | ('state', 'MD'), 52 | ('zip_code', '20770'), 53 | ]) 54 | res = verify( 55 | 'foo_id', 56 | address, 57 | ) 58 | 59 | expected = OrderedDict([ 60 | ('address', '6406 IVY LN'), 61 | ('city', 'GREENBELT'), 62 | ('state', 'MD'), 63 | ('zip5', '20770'), 64 | ('zip4', '1441'), 65 | ]) 66 | eq(res, expected) 67 | 68 | @fudge.patch('pyusps.urlutil.urlopen') 69 | def test_verify_zip_both(fake_urlopen): 70 | fake_urlopen = fake_urlopen.expects_call() 71 | 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""" 72 | fake_urlopen = fake_urlopen.with_args(req) 73 | res = StringIO(u""" 74 |
6406 IVY LNGREENBELTMD207701441
""") 75 | fake_urlopen.returns(res) 76 | 77 | address = OrderedDict([ 78 | ('address', '6406 Ivy Lane'), 79 | ('city', 'Greenbelt'), 80 | ('state', 'MD'), 81 | ('zip_code', '207701441'), 82 | ]) 83 | res = verify( 84 | 'foo_id', 85 | address, 86 | ) 87 | 88 | expected = OrderedDict([ 89 | ('address', '6406 IVY LN'), 90 | ('city', 'GREENBELT'), 91 | ('state', 'MD'), 92 | ('zip5', '20770'), 93 | ('zip4', '1441'), 94 | ]) 95 | eq(res, expected) 96 | 97 | @fudge.patch('pyusps.urlutil.urlopen') 98 | def test_verify_zip_dash(fake_urlopen): 99 | fake_urlopen = fake_urlopen.expects_call() 100 | 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""" 101 | fake_urlopen = fake_urlopen.with_args(req) 102 | res = StringIO(u""" 103 |
6406 IVY LNGREENBELTMD207701441
""") 104 | fake_urlopen.returns(res) 105 | 106 | address = OrderedDict([ 107 | ('address', '6406 Ivy Lane'), 108 | ('city', 'Greenbelt'), 109 | ('state', 'MD'), 110 | ('zip_code', '20770-1441'), 111 | ]) 112 | res = verify( 113 | 'foo_id', 114 | address 115 | ) 116 | 117 | expected = OrderedDict([ 118 | ('address', '6406 IVY LN'), 119 | ('city', 'GREENBELT'), 120 | ('state', 'MD'), 121 | ('zip5', '20770'), 122 | ('zip4', '1441'), 123 | ]) 124 | eq(res, expected) 125 | 126 | @fudge.patch('pyusps.urlutil.urlopen') 127 | def test_verify_zip_only(fake_urlopen): 128 | fake_urlopen = fake_urlopen.expects_call() 129 | 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""" 130 | fake_urlopen = fake_urlopen.with_args(req) 131 | res = StringIO(u""" 132 |
6406 IVY LNGREENBELTMD207701441
""") 133 | fake_urlopen.returns(res) 134 | 135 | address = OrderedDict([ 136 | ('address', '6406 Ivy Lane'), 137 | ('city', 'Greenbelt'), 138 | ('zip_code', '20770'), 139 | ]) 140 | res = verify( 141 | 'foo_id', 142 | address, 143 | ) 144 | 145 | expected = OrderedDict([ 146 | ('address', '6406 IVY LN'), 147 | ('city', 'GREENBELT'), 148 | ('state', 'MD'), 149 | ('zip5', '20770'), 150 | ('zip4', '1441'), 151 | ]) 152 | eq(res, expected) 153 | 154 | @fudge.patch('pyusps.urlutil.urlopen') 155 | def test_verify_state_only(fake_urlopen): 156 | fake_urlopen = fake_urlopen.expects_call() 157 | 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""" 158 | fake_urlopen = fake_urlopen.with_args(req) 159 | res = StringIO(u""" 160 |
6406 IVY LNGREENBELTMD207701441
""") 161 | fake_urlopen.returns(res) 162 | 163 | address = OrderedDict([ 164 | ('address', '6406 Ivy Lane'), 165 | ('city', 'Greenbelt'), 166 | ('state', 'MD'), 167 | ]) 168 | res = verify( 169 | 'foo_id', 170 | address, 171 | ) 172 | 173 | expected = OrderedDict([ 174 | ('address', '6406 IVY LN'), 175 | ('city', 'GREENBELT'), 176 | ('state', 'MD'), 177 | ('zip5', '20770'), 178 | ('zip4', '1441'), 179 | ]) 180 | eq(res, expected) 181 | 182 | @fudge.patch('pyusps.urlutil.urlopen') 183 | def test_verify_firm_name(fake_urlopen): 184 | fake_urlopen = fake_urlopen.expects_call() 185 | 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""" 186 | fake_urlopen = fake_urlopen.with_args(req) 187 | res = StringIO(u""" 188 |
XYZ CORP6406 IVY LNGREENBELTMD207701441
""") 189 | fake_urlopen.returns(res) 190 | 191 | address = OrderedDict([ 192 | ('firm_name', 'XYZ Corp'), 193 | ('address', '6406 Ivy Lane'), 194 | ('city', 'Greenbelt'), 195 | ('state', 'MD'), 196 | ]) 197 | res = verify( 198 | 'foo_id', 199 | address, 200 | ) 201 | 202 | expected = OrderedDict([ 203 | ('firm_name', 'XYZ CORP'), 204 | ('address', '6406 IVY LN'), 205 | ('city', 'GREENBELT'), 206 | ('state', 'MD'), 207 | ('zip5', '20770'), 208 | ('zip4', '1441'), 209 | ]) 210 | eq(res, expected) 211 | 212 | @fudge.patch('pyusps.urlutil.urlopen') 213 | def test_verify_address_extended(fake_urlopen): 214 | fake_urlopen = fake_urlopen.expects_call() 215 | 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""" 216 | fake_urlopen = fake_urlopen.with_args(req) 217 | res = StringIO(u""" 218 |
STE 126406 IVY LNGREENBELTMD207701441
""") 219 | fake_urlopen.returns(res) 220 | 221 | address = OrderedDict([ 222 | ('address', '6406 Ivy Lane'), 223 | ('address_extended', 'Suite 12'), 224 | ('city', 'Greenbelt'), 225 | ('state', 'MD'), 226 | ]) 227 | res = verify( 228 | 'foo_id', 229 | address, 230 | ) 231 | 232 | expected = OrderedDict([ 233 | ('address_extended', 'STE 12'), 234 | ('address', '6406 IVY LN'), 235 | ('city', 'GREENBELT'), 236 | ('state', 'MD'), 237 | ('zip5', '20770'), 238 | ('zip4', '1441'), 239 | ]) 240 | eq(res, expected) 241 | 242 | @fudge.patch('pyusps.urlutil.urlopen') 243 | def test_verify_urbanization(fake_urlopen): 244 | fake_urlopen = fake_urlopen.expects_call() 245 | 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""" 246 | fake_urlopen = fake_urlopen.with_args(req) 247 | res = StringIO(u""" 248 |
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""") 249 | fake_urlopen.returns(res) 250 | 251 | address = OrderedDict([ 252 | ('address', '6406 Ivy Lane'), 253 | ('urbanization', 'Puerto Rico'), 254 | ('city', 'Greenbelt'), 255 | ('state', 'MD'), 256 | ]) 257 | res = verify( 258 | 'foo_id', 259 | address, 260 | ) 261 | 262 | expected = OrderedDict([ 263 | ('address', '6406 IVY LN'), 264 | ('city', 'GREENBELT'), 265 | ('state', 'MD'), 266 | ('urbanization', 'PUERTO RICO'), 267 | ('zip5', '20770'), 268 | ('zip4', '1441'), 269 | ]) 270 | eq(res, expected) 271 | 272 | @fudge.patch('pyusps.urlutil.urlopen') 273 | def test_verify_multiple(fake_urlopen): 274 | fake_urlopen = fake_urlopen.expects_call() 275 | 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""" 276 | fake_urlopen = fake_urlopen.with_args(req) 277 | res = StringIO(u""" 278 |
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""") 279 | fake_urlopen.returns(res) 280 | 281 | addresses = [ 282 | OrderedDict([ 283 | ('address', '6406 Ivy Lane'), 284 | ('city', 'Greenbelt'), 285 | ('state', 'MD'), 286 | ]), 287 | OrderedDict([ 288 | ('address', '8 Wildwood Drive'), 289 | ('city', 'Old Lyme'), 290 | ('state', 'CT'), 291 | ]), 292 | ] 293 | res = verify( 294 | 'foo_id', 295 | *addresses 296 | ) 297 | 298 | expected = [ 299 | OrderedDict([ 300 | ('address', '6406 IVY LN'), 301 | ('city', 'GREENBELT'), 302 | ('state', 'MD'), 303 | ('zip5', '20770'), 304 | ('zip4', '1441'), 305 | ]), 306 | OrderedDict([ 307 | ('address', '8 WILDWOOD DR'), 308 | ('city', 'OLD LYME'), 309 | ('state', 'CT'), 310 | ('zip5', '06371'), 311 | ('zip4', '1844'), 312 | ]), 313 | ] 314 | eq(res, expected) 315 | 316 | @fudge.patch('pyusps.urlutil.urlopen') 317 | def test_verify_more_than_5(fake_urlopen): 318 | addresses = [ 319 | OrderedDict(), 320 | OrderedDict(), 321 | OrderedDict(), 322 | OrderedDict(), 323 | OrderedDict(), 324 | OrderedDict(), 325 | ] 326 | 327 | msg = assert_raises( 328 | ValueError, 329 | verify, 330 | 'foo_id', 331 | *addresses 332 | ) 333 | 334 | eq(str(msg), 'Only 5 addresses are allowed per request') 335 | 336 | @fudge.patch('pyusps.urlutil.urlopen') 337 | def test_verify_api_root_error(fake_urlopen): 338 | fake_urlopen = fake_urlopen.expects_call() 339 | 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""" 340 | fake_urlopen = fake_urlopen.with_args(req) 341 | res = StringIO(u""" 342 | 80040b1a 343 | Authorization failure. Perhaps username and/or password is incorrect. 344 | UspsCom::DoAuth 345 | """) 346 | fake_urlopen.returns(res) 347 | 348 | address = OrderedDict([ 349 | ('address', '6406 Ivy Lane'), 350 | ('city', 'Greenbelt'), 351 | ('state', 'MD'), 352 | ]) 353 | msg = assert_raises( 354 | ValueError, 355 | verify, 356 | 'foo_id', 357 | address 358 | ) 359 | 360 | expected = ('80040b1a: Authorization failure. Perhaps username ' 361 | 'and/or password is incorrect.' 362 | ) 363 | eq(str(msg), expected) 364 | 365 | @fudge.patch('pyusps.urlutil.urlopen') 366 | def test_verify_api_address_error_single(fake_urlopen): 367 | fake_urlopen = fake_urlopen.expects_call() 368 | 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""" 369 | fake_urlopen = fake_urlopen.with_args(req) 370 | res = StringIO(u""" 371 |
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""") 372 | fake_urlopen.returns(res) 373 | 374 | address = OrderedDict([ 375 | ('address', '6406 Ivy Lane'), 376 | ('city', 'Greenbelt'), 377 | ('state', 'NJ'), 378 | ]) 379 | msg = assert_raises( 380 | ValueError, 381 | verify, 382 | 'foo_id', 383 | address 384 | ) 385 | 386 | expected = '-2147219401: Address Not Found.' 387 | eq(str(msg), expected) 388 | 389 | @fudge.patch('pyusps.urlutil.urlopen') 390 | def test_verify_api_address_error_multiple(fake_urlopen): 391 | fake_urlopen = fake_urlopen.expects_call() 392 | 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""" 393 | fake_urlopen = fake_urlopen.with_args(req) 394 | res = StringIO(u""" 395 |
6406 IVY LNGREENBELTMD207701441
-2147219400API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllInvalid City.1000440
""") 396 | fake_urlopen.returns(res) 397 | 398 | addresses = [ 399 | OrderedDict([ 400 | ('address', '6406 Ivy Lane'), 401 | ('city', 'Greenbelt'), 402 | ('state', 'MD'), 403 | ]), 404 | OrderedDict([ 405 | ('address', '8 Wildwood Drive'), 406 | ('city', 'Old Lyme'), 407 | ('state', 'NJ'), 408 | ]), 409 | ] 410 | res = verify( 411 | 'foo_id', 412 | *addresses 413 | ) 414 | 415 | # eq does not work with exceptions. Process each item manually. 416 | eq(len(res), 2) 417 | eq( 418 | res[0], 419 | OrderedDict([ 420 | ('address', '6406 IVY LN'), 421 | ('city', 'GREENBELT'), 422 | ('state', 'MD'), 423 | ('zip5', '20770'), 424 | ('zip4', '1441'), 425 | ]), 426 | ) 427 | assert_errors_equal( 428 | res[1], 429 | ValueError('-2147219400: Invalid City.'), 430 | ) 431 | 432 | @fudge.patch('pyusps.urlutil.urlopen') 433 | def test_verify_api_empty_error(fake_urlopen): 434 | fake_urlopen = fake_urlopen.expects_call() 435 | 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""" 436 | fake_urlopen = fake_urlopen.with_args(req) 437 | res = StringIO(u""" 438 | """) 439 | fake_urlopen.returns(res) 440 | 441 | address = OrderedDict([ 442 | ('address', '6406 Ivy Lane'), 443 | ('city', 'Greenbelt'), 444 | ('state', 'NJ'), 445 | ]) 446 | msg = assert_raises( 447 | TypeError, 448 | verify, 449 | 'foo_id', 450 | address 451 | ) 452 | 453 | expected = 'Could not find any address or error information' 454 | eq(str(msg), expected) 455 | 456 | @fudge.patch('pyusps.urlutil.urlopen') 457 | def test_verify_api_order_error(fake_urlopen): 458 | fake_urlopen = fake_urlopen.expects_call() 459 | 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""" 460 | fake_urlopen = fake_urlopen.with_args(req) 461 | res = StringIO(u""" 462 |
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""") 463 | fake_urlopen.returns(res) 464 | 465 | addresses = [ 466 | OrderedDict([ 467 | ('address', '6406 Ivy Lane'), 468 | ('city', 'Greenbelt'), 469 | ('state', 'MD'), 470 | ]), 471 | OrderedDict([ 472 | ('address', '8 Wildwood Drive'), 473 | ('city', 'Old Lyme'), 474 | ('state', 'CT'), 475 | ]), 476 | ] 477 | msg = assert_raises( 478 | IndexError, 479 | verify, 480 | 'foo_id', 481 | *addresses 482 | ) 483 | 484 | expected = ('The addresses returned are not in the same order ' 485 | 'they were requested' 486 | ) 487 | eq(str(msg), expected) 488 | -------------------------------------------------------------------------------- /pyusps/test/util.py: -------------------------------------------------------------------------------- 1 | def assert_raises(excClass, callableObj, *args, **kwargs): 2 | """ 3 | Like unittest.TestCase.assertRaises, but returns the exception. 4 | """ 5 | try: 6 | callableObj(*args, **kwargs) 7 | except excClass as e: 8 | return e 9 | else: 10 | if hasattr(excClass,'__name__'): excName = excClass.__name__ 11 | else: excName = str(excClass) 12 | raise AssertionError("%s not raised" % excName) 13 | 14 | def assert_errors_equal(error_1, error_2): 15 | assert type(error_1) == type(error_2) 16 | assert error_1.args, error_2.args 17 | -------------------------------------------------------------------------------- /pyusps/urlutil.py: -------------------------------------------------------------------------------- 1 | # `fudge.patch` in `pyusps.test.test_address_information` needs the full 2 | # module path as well as the function name as its argument, e.g., 3 | # "urllib2.urlopen". Create a normalized module path here for 4 | # urllib2/urllib functions in order to support both Python 2 and Python 3. 5 | 6 | try: 7 | from urllib.request import urlopen as _urlopen 8 | except ImportError: 9 | from urllib2 import urlopen as _urlopen 10 | 11 | try: 12 | from urllib.parse import urlencode as _urlencode 13 | except ImportError: 14 | from urllib import urlencode as _urlencode 15 | 16 | urlopen = _urlopen 17 | urlencode = _urlencode 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from setuptools import setup, find_packages 3 | import os 4 | 5 | EXTRAS_REQUIRES = dict( 6 | test=[ 7 | 'fudge>=1.1.1', 8 | 'nose>=1.3.7', 9 | ], 10 | dev=[ 11 | 'ipython>=5.5.0', 12 | ], 13 | ) 14 | 15 | # Pypi package documentation 16 | root = os.path.dirname(__file__) 17 | path = os.path.join(root, 'README.rst') 18 | with open(path) as fp: 19 | long_description = fp.read() 20 | 21 | setup( 22 | name='pyusps', 23 | version='0.0.7', 24 | description='pyusps -- Python bindings for the USPS Ecommerce APIs', 25 | long_description=long_description, 26 | author='Andres Buritica', 27 | author_email='andres@thelinuxkid.com', 28 | maintainer='Andres Buritica', 29 | maintainer_email='andres@thelinuxkid.com', 30 | url='https://github.com/thelinuxkid/pyusps', 31 | license='MIT', 32 | packages = find_packages(), 33 | namespace_packages = ['pyusps'], 34 | test_suite='nose.collector', 35 | install_requires=[ 36 | 'setuptools>=0.6c11', 37 | 'lxml>=2.3.3', 38 | ], 39 | extras_require=EXTRAS_REQUIRES, 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'Intended Audience :: Developers', 43 | 'Natural Language :: English', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 2.7', 47 | 'Programming Language :: Python :: 3.5' 48 | ], 49 | ) 50 | --------------------------------------------------------------------------------