diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..ef87bfd --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,64 @@ +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 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 + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a079595 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# 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. +- 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. +- Testing on GitHub Actions! +- Type hints \ No newline at end of file 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/README.rst b/README.rst index faa4ccd..e1446aa 100644 --- a/README.rst +++ b/README.rst @@ -22,14 +22,16 @@ 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 -------- -Each address is a dict containing the following required keys: +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-like (e.g. supports `__getitem__()`) containing the following +required keys: :address: The street address :city: The city @@ -44,13 +46,10 @@ The following keys are optional: :address_extended: An apartment, suite number, etc :urbanization: For Puerto Rico addresses only - - 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 `dict`s or `USPSError`s. Each `dict` will always contain the following keys: :address: The street address @@ -60,7 +59,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 @@ -70,101 +69,60 @@ 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 +------- -Examples --------- +Mutiple addresses, one of them isn't found so an error is returned:: + + >>> from pyusps import address_information + + >>> addrs = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + }, + { + "address": "8 Wildwood Drive", + "city": "Old Lyme", + "state": "NJ", + }, + ] + >>> results = address_information.verify('foo_id', addrs) + >>> results + [ + { + '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.'), + ] + >>> results[1].code + '-2147219400' + >>> res[1].description + 'Invalid City.' -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'), - ]), - ] - -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. '), - ] Reference --------- 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/pyusps/address_information.py b/pyusps/address_information.py index 31964c0..ac2eb16 100644 --- a/pyusps/address_information.py +++ b/pyusps/address_information.py @@ -1,135 +1,133 @@ -from collections import OrderedDict +from collections.abc import Mapping +from typing import Any, Iterable, Union from lxml import etree -import pyusps.urlutil +from urllib.parse import urlencode +from urllib.request import urlopen +ADDRESS_MAX = 5 -api_url = 'https://production.shippingapis.com/ShippingAPI.dll' -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. -def _find_error(root): - if root.tag == 'Error': - num = root.find('Number') - desc = root.find('Description') - return (num, desc) + Inherits from ValueError. Also has attributes `code: str` and `description: str`. + """ + code: str + description: str -def _get_error(error): - (num, desc) = error - return ValueError( - '{num}: {desc}'.format( - num=num.text, - desc=desc.text, - ) - ) + def __init__(self, code: str, description: str) -> None: + self.code = code + self.description = description + super().__init__(f"{code}: {description}") -def _get_address_error(address): - error = address.find('Error') - if error is not None: - error = _find_error(error) - return _get_error(error) + def __eq__(self, o: object) -> bool: + return ( + isinstance(o, USPSError) and + self.code == o.code and + self.description == o.description + ) -def _parse_address(address): - result = OrderedDict() +Result = Union[dict, USPSError] + +def _get_error(node): + if node.tag != 'Error': + return None + code = node.find('Number').text.strip() + description = node.find('Description').text.strip() + return USPSError(code, description) + +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: etree._Element) -> dict: + result = {} + # 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_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): +def _process_multiple(addresses: "list[etree._Element]") -> "list[Result]": results = [] - count = 0 - for address in addresses: + 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 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) + raise RuntimeError(msg) + result = _parse_address(address) results.append(result) - count += 1 return results -def _parse_response(res): +def _parse_response(res: etree._ElementTree) -> "list[Result]": # General error, e.g., authorization - error = _find_error(res.getroot()) + error = _get_error(res.getroot()) if error is not None: - raise _get_error(error) - - results = res.findall('Address') - length = len(results) - if length == 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): - params = OrderedDict([ - ('API', 'Verify'), - ('XML', etree.tostring(xml)), - ]) - url = '{api_url}?{params}'.format( - api_url=api_url, - params=pyusps.urlutil.urlencode(params), - ) + raise error - res = pyusps.urlutil.urlopen(url) - res = etree.parse(res) + elements = res.findall('Address') + if len(elements) == 0: + raise RuntimeError('Could not find any address or error information') + return _process_multiple(elements) + +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) + tree = etree.parse(res) + return tree + +def _convert_input(input: "Iterable[Mapping]") -> "list[Mapping]": + 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 - return res +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, - *args - ): +def _create_xml(user_id: str, addresses: "list[Mapping]") -> etree._Element: 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(args): + 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) @@ -184,9 +182,11 @@ def _create_xml( return root -def verify(user_id, *args): - xml = _create_xml(user_id, *args) - res = _get_response(xml) - res = _parse_response(res) - +def verify(user_id: str, addresses: "Iterable[Mapping]") -> "list[Union[dict, USPSError]]": + addresses = _convert_input(addresses) + if len(addresses) == 0: + return [] + 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 fe2b888..132c5d0 100644 --- a/pyusps/test/test_address_information.py +++ b/pyusps/test/test_address_information.py @@ -1,487 +1,472 @@ +# type: ignore + import fudge -from collections import OrderedDict 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') +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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '207701441'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ('zip_code', '20770-1441'), - ]) - res = verify( - 'foo_id', - address - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('zip_code', '20770'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('firm_name', 'XYZ Corp'), - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
XYZ CORP6406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') +@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) - - address = OrderedDict([ - ('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'), - ]) + res = u"""
STE 126406 IVY LNGREENBELTMD207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + address = [ + { + "address": "6406 Ivy Lane", + "address_extended": "Suite 12", + "city": "Greenbelt", + "state": "MD", + } + ] + res = verify('foo_id', address) + + 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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('urbanization', 'Puerto Rico'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - res = verify( - 'foo_id', - address, - ) + res = u"""
6406 IVY LNGREENBELTMDPUERTO RICO207701441
""" + setup_urlopen_mock(fake_urlopen, req, res) + + 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') + +@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) - - addresses = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'CT'), - ]), - ] - res = verify( - 'foo_id', - *addresses - ) + res = u"""
6406 IVY LNGREENBELTMD207701441
8 WILDWOOD DROLD LYMECT063711844
""" + setup_urlopen_mock(fake_urlopen, req, res) + + addresses_list = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + }, + { + "address": "8 Wildwood Drive", + "city": "Old Lyme", + "state": "CT", + }, + ] + addresses_generator = (a for a in addresses_list) 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'), - ]), - ] - eq(res, expected) - -@fudge.patch('pyusps.urlutil.urlopen') + { + "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) + + +def test_empty_input(): + """We can handle empty input.""" + result = verify("user_id", []) + expected = [] + eq(result, expected) + + +@fudge.patch('pyusps.address_information.urlopen') def test_verify_more_than_5(fake_urlopen): - addresses = [ - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - OrderedDict(), - ] - - msg = assert_raises( + addr = { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } + addresses = [addr] * 6 + + err = assert_raises( ValueError, verify, 'foo_id', - *addresses + addresses ) + expected = ValueError('Only 5 addresses are allowed per request') + assert_errors_equal(err, expected) - eq(str(msg), 'Only 5 addresses are allowed per request') -@fudge.patch('pyusps.urlutil.urlopen') +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) + + +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]) - msg = assert_raises( - ValueError, + """ + setup_urlopen_mock(fake_urlopen, req, res) + + address = { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "MD", + } + err = assert_raises( + USPSError, verify, 'foo_id', - address + [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') +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'NJ'), - ]) - msg = assert_raises( - ValueError, - verify, - 'foo_id', - address - ) - - expected = '-2147219401: Address Not Found.' - eq(str(msg), expected) - -@fudge.patch('pyusps.urlutil.urlopen') + res = u"""
-2147219401API_AddressCleancAddressClean.CleanAddress2;SOLServer.CallAddressDllAddress Not Found.1000440
""" + setup_urlopen_mock(fake_urlopen, req, res) + + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "NJ", + } + ] + res = verify('foo_id', address) + + eq(len(res), 1) + expected = USPSError("-2147219401", "Address Not Found.") + eq(res[0], expected) + +@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 = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'NJ'), - ]), - ] - res = verify( - 'foo_id', - *addresses - ) + { + "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'), - ]), - ) - assert_errors_equal( - res[1], - ValueError('-2147219400: Invalid City.'), - ) - -@fudge.patch('pyusps.urlutil.urlopen') + { + "address": "6406 IVY LN", + "city": "GREENBELT", + "state": "MD", + "zip5": "20770", + "zip4": "1441", + }, + ) + expected = USPSError("-2147219400", "Invalid City.") + eq(res[1], expected) + +@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) - - address = OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'NJ'), - ]) - msg = assert_raises( - TypeError, + res = u"""""" + setup_urlopen_mock(fake_urlopen, req, res) + + address = [ + { + "address": "6406 Ivy Lane", + "city": "Greenbelt", + "state": "NJ", + } + ] + err = assert_raises( + RuntimeError, verify, 'foo_id', address ) - expected = 'Could not find any address or error information' - eq(str(msg), expected) + 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""" - 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 = [ - OrderedDict([ - ('address', '6406 Ivy Lane'), - ('city', 'Greenbelt'), - ('state', 'MD'), - ]), - OrderedDict([ - ('address', '8 Wildwood Drive'), - ('city', 'Old Lyme'), - ('state', 'CT'), - ]), - ] - msg = assert_raises( - IndexError, + { + 'address': '6406 Ivy Lane', + 'city': 'Greenbelt', + 'state': 'MD', + }, + { + 'address': '8 Wildwood Drive', + 'city': 'Old Lyme', + 'state': 'CT', + } + ] + err = assert_raises( + RuntimeError, verify, 'foo_id', - *addresses + addresses ) - - expected = ('The addresses returned are not in the same order ' - 'they were requested' - ) - eq(str(msg), expected) + expected = RuntimeError( + 'The addresses returned are not in the same order they were requested' + ) + assert_errors_equal(err, expected) 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 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 e7fbdc5..f13cf21 100755 --- a/setup.py +++ b/setup.py @@ -1,49 +1,3 @@ -#!/usr/bin/python -from setuptools import setup, find_packages -import os - -EXTRAS_REQUIRES = dict( - test=[ - 'fudge>=1.1.1', - '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()