├── .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 LNGREENBELTMD2077014418 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 LNGREENBELTMD2077014418 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 |
--------------------------------------------------------------------------------