From d9f272e2fc0ebd9bf03347baceaa08ba99e992e2 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Tue, 27 Jul 2010 12:13:46 -0700 Subject: [PATCH 01/11] move shared functions to global errors and utils modules --- usps/addressinformation/base.py | 30 +++--------------------------- usps/errors.py | 9 +++++++++ usps/utils.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 usps/errors.py create mode 100644 usps/utils.py diff --git a/usps/addressinformation/base.py b/usps/addressinformation/base.py index d6c468f..cc02f77 100644 --- a/usps/addressinformation/base.py +++ b/usps/addressinformation/base.py @@ -3,38 +3,14 @@ ''' import urllib, urllib2 +from usps.utils import utf8urlencode, xmltodict, dicttoxml +from usps.errors import USPSXMLError + try: from xml.etree import ElementTree as ET except ImportError: from elementtree import ElementTree as ET -def utf8urlencode(data): - ret = dict() - for key, value in data.iteritems(): - ret[key] = value.encode('utf8') - return urllib.urlencode(ret) - -def dicttoxml(dictionary, parent, tagname, attributes=None): - element = ET.SubElement(parent, tagname) - if attributes: #USPS likes things in a certain order! - for key in attributes: - ET.SubElement(element, key).text = dictionary.get(key, '') - else: - for key, value in dictionary.iteritems(): - ET.SubElement(element, key).text = value - return element - -def xmltodict(element): - ret = dict() - for item in element.getchildren(): - ret[item.tag] = item.text - return ret - -class USPSXMLError(Exception): - def __init__(self, element): - self.info = xmltodict(element) - super(USPSXMLError, self).__init__(self.info['Description']) - class USPSAddressService(object): SERVICE_NAME = None API = None diff --git a/usps/errors.py b/usps/errors.py new file mode 100644 index 0000000..e835ecd --- /dev/null +++ b/usps/errors.py @@ -0,0 +1,9 @@ +""" +Global errors module for USPS app +""" +from usps.utils import xmltodict + +class USPSXMLError(Exception): + def __init__(self, element): + self.info = xmltodict(element) + super(USPSXMLError, self).__init__(self.info['Description']) \ No newline at end of file diff --git a/usps/utils.py b/usps/utils.py new file mode 100644 index 0000000..267537d --- /dev/null +++ b/usps/utils.py @@ -0,0 +1,29 @@ +""" +Utility functions for use in USPS app +""" +try: + from xml.etree import ElementTree as ET +except ImportError: + from elementtree import ElementTree as ET + +def utf8urlencode(data): + ret = dict() + for key, value in data.iteritems(): + ret[key] = value.encode('utf8') + return urllib.urlencode(ret) + +def dicttoxml(dictionary, parent, tagname, attributes=None): + element = ET.SubElement(parent, tagname) + if attributes: #USPS likes things in a certain order! + for key in attributes: + ET.SubElement(element, key).text = dictionary.get(key, '') + else: + for key, value in dictionary.iteritems(): + ET.SubElement(element, key).text = value + return element + +def xmltodict(element): + ret = dict() + for item in element.getchildren(): + ret[item.tag] = item.text + return ret \ No newline at end of file From 58c04f51d5d33c20437f95255ea7bd9f3e0fa205 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Wed, 28 Jul 2010 14:34:45 -0700 Subject: [PATCH 02/11] fix dicttoxml to take an arbitrarily nested dictionary and transform it to xml --- .hgignore | 2 ++ tests/tests.py | 1 - usps/addressinformation/base.py | 57 +++------------------------------ usps/base.py | 49 ++++++++++++++++++++++++++++ usps/ratecalculator/__init__.py | 3 ++ usps/ratecalculator/base.py | 46 ++++++++++++++++++++++++++ usps/utils.py | 39 +++++++++++++++++++--- 7 files changed, 139 insertions(+), 58 deletions(-) create mode 100644 .hgignore create mode 100644 usps/base.py create mode 100644 usps/ratecalculator/__init__.py create mode 100644 usps/ratecalculator/base.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..41f1d6d --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +syntax: glob +*.pyc \ No newline at end of file diff --git a/tests/tests.py b/tests/tests.py index ed1e16b..69bbe6a 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,5 +1,4 @@ import unittest - from usps.addressinformation import * USERID = None diff --git a/usps/addressinformation/base.py b/usps/addressinformation/base.py index cc02f77..f1b25db 100644 --- a/usps/addressinformation/base.py +++ b/usps/addressinformation/base.py @@ -1,58 +1,9 @@ ''' See http://www.usps.com/webtools/htm/Address-Information.htm for complete documentation of the API ''' +from usps.base import USPSService -import urllib, urllib2 -from usps.utils import utf8urlencode, xmltodict, dicttoxml -from usps.errors import USPSXMLError - -try: - from xml.etree import ElementTree as ET -except ImportError: - from elementtree import ElementTree as ET - -class USPSAddressService(object): - SERVICE_NAME = None - API = None - CHILD_XML_NAME = None - PARAMETERS = None - - def __init__(self, url): - self.url = url - - def submit_xml(self, xml): - data = {'XML':ET.tostring(xml), - 'API':self.API} - response = urllib2.urlopen(self.url, utf8urlencode(data)) - root = ET.parse(response).getroot() - if root.tag == 'Error': - raise USPSXMLError(root) - error = root.find('.//Error') - if error: - raise USPSXMLError(error) - return root - - def parse_xml(self, xml): - items = list() - for item in xml.getchildren():#xml.findall(self.SERVICE_NAME+'Response'): - items.append(xmltodict(item)) - return items - - def make_xml(self, userid, addresses): - root = ET.Element(self.SERVICE_NAME+'Request') - root.attrib['USERID'] = userid - index = 0 - for address_dict in addresses: - address_xml = dicttoxml(address_dict, root, self.CHILD_XML_NAME, self.PARAMETERS) - address_xml.attrib['ID'] = str(index) - index += 1 - return root - - def execute(self, userid, addresses): - xml = self.make_xml(userid, addresses) - return self.parse_xml(self.submit_xml(xml)) - -class AddressValidate(USPSAddressService): +class AddressValidate(USPSService): SERVICE_NAME = 'AddressValidate' CHILD_XML_NAME = 'Address' API = 'Verify' @@ -64,7 +15,7 @@ class AddressValidate(USPSAddressService): 'Zip5', 'Zip4',] -class ZipCodeLookup(USPSAddressService): +class ZipCodeLookup(USPSService): SERVICE_NAME = 'ZipCodeLookup' CHILD_XML_NAME = 'Address' API = 'ZipCodeLookup' @@ -74,7 +25,7 @@ class ZipCodeLookup(USPSAddressService): 'City', 'State',] -class CityStateLookup(USPSAddressService): +class CityStateLookup(USPSService): SERVICE_NAME = 'CityStateLookup' CHILD_XML_NAME = 'ZipCode' API = 'CityStateLookup' diff --git a/usps/base.py b/usps/base.py new file mode 100644 index 0000000..c8abe05 --- /dev/null +++ b/usps/base.py @@ -0,0 +1,49 @@ +import urllib, urllib2 +from usps.utils import utf8urlencode, xmltodict, dicttoxml +from usps.errors import USPSXMLError + +try: + from xml.etree import ElementTree as ET +except ImportError: + from elementtree import ElementTree as ET + +class USPSService(object): + SERVICE_NAME = None + API = None + CHILD_XML_NAME = None + PARAMETERS = None + + def __init__(self, url): + self.url = url + + def submit_xml(self, xml): + data = {'XML':ET.tostring(xml), + 'API':self.API} + response = urllib2.urlopen(self.url, utf8urlencode(data)) + root = ET.parse(response).getroot() + if root.tag == 'Error': + raise USPSXMLError(root) + error = root.find('.//Error') + if error: + raise USPSXMLError(error) + return root + + def parse_xml(self, xml): + items = list() + for item in xml.getchildren():#xml.findall(self.SERVICE_NAME+'Response'): + items.append(xmltodict(item)) + return items + + def make_xml(self, userid, data): + root = ET.Element(self.SERVICE_NAME+'Request') + root.attrib['USERID'] = userid + index = 0 + for data_dict in data: + data_xml = dicttoxml(data_dict, root, self.CHILD_XML_NAME, self.PARAMETERS) + data_xml.attrib['ID'] = str(index) + index += 1 + return root + + def execute(self, userid, data): + xml = self.make_xml(userid, data) + return self.parse_xml(self.submit_xml(xml)) \ No newline at end of file diff --git a/usps/ratecalculator/__init__.py b/usps/ratecalculator/__init__.py new file mode 100644 index 0000000..fa2bb96 --- /dev/null +++ b/usps/ratecalculator/__init__.py @@ -0,0 +1,3 @@ +""" +USPS rate calculator module +""" \ No newline at end of file diff --git a/usps/ratecalculator/base.py b/usps/ratecalculator/base.py new file mode 100644 index 0000000..aa31566 --- /dev/null +++ b/usps/ratecalculator/base.py @@ -0,0 +1,46 @@ +""" +""" + +from usps.base import USPSService + +class DomesticRateCalculator(USPSService): + """ + Calculator for domestic shipping rates + """ + SERVICE_NAME = 'RateV3Request' + CHILD_XML_NAME = 'Package' + API = 'RateV3' + PARAMETERS = ['Service', + 'FirstClassMailType', + 'ZipOrigination', + 'ZipDestination', + 'Pounds', + 'Ounces', + 'Container', + 'Size', + 'Width', + 'Length', + 'Height', + 'Girth', + 'Machinable', + 'ReturnLocations', + 'ShipDate', + ] + + +class InternationalRateCalculator(USPSService): + """ + Calculator for international shipping rates + """ + SERVICE_NAME = 'IntlRateRequest' + CHILD_XML_NAME = 'Package' + API = 'IntlRate' + PARAMETERS = [ + 'Pounds', + 'Ounces', + 'Machinable', + 'MailType', + 'GXG', + 'ValueOfContents', + 'Country', + ] \ No newline at end of file diff --git a/usps/utils.py b/usps/utils.py index 267537d..21bb13a 100644 --- a/usps/utils.py +++ b/usps/utils.py @@ -1,28 +1,59 @@ """ Utility functions for use in USPS app """ +import urllib try: from xml.etree import ElementTree as ET except ImportError: from elementtree import ElementTree as ET def utf8urlencode(data): + """ + utf8 URL encode the given dictionary's data + + @param data: a dictionary of data to be encoded + """ ret = dict() for key, value in data.iteritems(): ret[key] = value.encode('utf8') return urllib.urlencode(ret) -def dicttoxml(dictionary, parent, tagname, attributes=None): - element = ET.SubElement(parent, tagname) +def dicttoxml(dictionary, tagname, parent=None, attributes=None): + """ + Transform a dictionary to xml + + @param dictionary: a dictionary + @param parent: a parent node + @return: XML serialization of the given dictionary + """ + if parent: + element = ET.SubElement(parent, tagname) + else: + element = ET.Element(tagname) + if attributes: #USPS likes things in a certain order! for key in attributes: - ET.SubElement(element, key).text = dictionary.get(key, '') + value = dictionary.get(key, '') + if type(value).__name__ == 'dict': + elem = dicttoxml(value, key) + element.append(elem) + else: + ET.SubElement(element, key).text = value else: for key, value in dictionary.iteritems(): - ET.SubElement(element, key).text = value + if type(value).__name__ == 'dict': + elem = dicttoxml(value, key) + element.append(elem) + else: + ET.SubElement(element, key).text = value return element def xmltodict(element): + """ + Transform an xml fragment into a python dictionary + + @param element: an XML fragment + """ ret = dict() for item in element.getchildren(): ret[item.tag] = item.text From ef25878d2feb89b50d377fd63fdf01cd5f47495e Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Wed, 28 Jul 2010 15:12:41 -0700 Subject: [PATCH 03/11] restructure -- move api connectors to api module, inherit from base api connector, keep global functions etc.. in usps module --- tests/tests.py | 3 ++- usps/{addressinformation => api}/__init__.py | 4 +--- .../{addressinformation/base.py => api/addressinformation.py} | 2 +- usps/{ => api}/base.py | 0 usps/{ratecalculator/base.py => api/ratecalculator.py} | 4 ++-- usps/ratecalculator/__init__.py | 3 --- 6 files changed, 6 insertions(+), 10 deletions(-) rename usps/{addressinformation => api}/__init__.py (70%) rename usps/{addressinformation/base.py => api/addressinformation.py} (95%) rename usps/{ => api}/base.py (100%) rename usps/{ratecalculator/base.py => api/ratecalculator.py} (94%) delete mode 100644 usps/ratecalculator/__init__.py diff --git a/tests/tests.py b/tests/tests.py index 69bbe6a..2f484ce 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,5 +1,6 @@ import unittest -from usps.addressinformation import * +from usps.api import USPS_CONNECTION_TEST +from usps.api.addressinformation import AddressValidate, ZipCodeLookup, CityStateLookup USERID = None diff --git a/usps/addressinformation/__init__.py b/usps/api/__init__.py similarity index 70% rename from usps/addressinformation/__init__.py rename to usps/api/__init__.py index 6733242..68199e5 100644 --- a/usps/addressinformation/__init__.py +++ b/usps/api/__init__.py @@ -1,5 +1,3 @@ -from base import AddressValidate, ZipCodeLookup, CityStateLookup, USPSXMLError - USPS_CONNECTION = 'http://production.shippingapis.com/ShippingAPI.dll' USPS_CONNECTION_TEST_SECURE = 'https://secure.shippingapis.com/ShippingAPITest.dll' -USPS_CONNECTION_TEST = 'http://testing.shippingapis.com/ShippingAPITest.dll' +USPS_CONNECTION_TEST = 'http://testing.shippingapis.com/ShippingAPITest.dll' \ No newline at end of file diff --git a/usps/addressinformation/base.py b/usps/api/addressinformation.py similarity index 95% rename from usps/addressinformation/base.py rename to usps/api/addressinformation.py index f1b25db..975de60 100644 --- a/usps/addressinformation/base.py +++ b/usps/api/addressinformation.py @@ -1,7 +1,7 @@ ''' See http://www.usps.com/webtools/htm/Address-Information.htm for complete documentation of the API ''' -from usps.base import USPSService +from usps.api.base import USPSService class AddressValidate(USPSService): SERVICE_NAME = 'AddressValidate' diff --git a/usps/base.py b/usps/api/base.py similarity index 100% rename from usps/base.py rename to usps/api/base.py diff --git a/usps/ratecalculator/base.py b/usps/api/ratecalculator.py similarity index 94% rename from usps/ratecalculator/base.py rename to usps/api/ratecalculator.py index aa31566..fd874fe 100644 --- a/usps/ratecalculator/base.py +++ b/usps/api/ratecalculator.py @@ -1,7 +1,7 @@ """ +Rate Calculator classes """ - -from usps.base import USPSService +from usps.api.base import USPSService class DomesticRateCalculator(USPSService): """ diff --git a/usps/ratecalculator/__init__.py b/usps/ratecalculator/__init__.py deleted file mode 100644 index fa2bb96..0000000 --- a/usps/ratecalculator/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -USPS rate calculator module -""" \ No newline at end of file From 2e4c833e90ee9e7feb383ee63d39743c0ce4daca Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Wed, 28 Jul 2010 16:27:48 -0700 Subject: [PATCH 04/11] more fixes to dicttoxml, add tracking wrapper --- .hgignore | 4 +++- usps/api/base.py | 12 +++++++----- usps/api/ratecalculator.py | 4 +++- usps/api/tracking.py | 31 +++++++++++++++++++++++++++++++ usps/utils.py | 12 ++++++------ 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 usps/api/tracking.py diff --git a/.hgignore b/.hgignore index 41f1d6d..d954c49 100644 --- a/.hgignore +++ b/.hgignore @@ -1,2 +1,4 @@ syntax: glob -*.pyc \ No newline at end of file +*.pyc +*.orig +python_usps.egg-info \ No newline at end of file diff --git a/usps/api/base.py b/usps/api/base.py index c8abe05..c011f1d 100644 --- a/usps/api/base.py +++ b/usps/api/base.py @@ -8,10 +8,10 @@ from elementtree import ElementTree as ET class USPSService(object): - SERVICE_NAME = None - API = None - CHILD_XML_NAME = None - PARAMETERS = None + SERVICE_NAME = '' + API = '' + CHILD_XML_NAME = '' + PARAMETERS = [] def __init__(self, url): self.url = url @@ -39,8 +39,10 @@ def make_xml(self, userid, data): root.attrib['USERID'] = userid index = 0 for data_dict in data: - data_xml = dicttoxml(data_dict, root, self.CHILD_XML_NAME, self.PARAMETERS) + data_xml = dicttoxml(data_dict, self.CHILD_XML_NAME, self.PARAMETERS) data_xml.attrib['ID'] = str(index) + + root.append(data_xml) index += 1 return root diff --git a/usps/api/ratecalculator.py b/usps/api/ratecalculator.py index fd874fe..3015ab6 100644 --- a/usps/api/ratecalculator.py +++ b/usps/api/ratecalculator.py @@ -7,7 +7,7 @@ class DomesticRateCalculator(USPSService): """ Calculator for domestic shipping rates """ - SERVICE_NAME = 'RateV3Request' + SERVICE_NAME = 'RateV3' CHILD_XML_NAME = 'Package' API = 'RateV3' PARAMETERS = ['Service', @@ -31,6 +31,8 @@ class DomesticRateCalculator(USPSService): class InternationalRateCalculator(USPSService): """ Calculator for international shipping rates + + @todo - deal with nested GXG data (Length,Width,Height,POBoxFlag,GiftFlag) """ SERVICE_NAME = 'IntlRateRequest' CHILD_XML_NAME = 'Package' diff --git a/usps/api/tracking.py b/usps/api/tracking.py new file mode 100644 index 0000000..7442be7 --- /dev/null +++ b/usps/api/tracking.py @@ -0,0 +1,31 @@ +""" +Track and Confirm class +""" +from usps.api.base import USPSService + +try: + from xml.etree import ElementTree as ET +except ImportError: + from elementtree import ElementTree as ET + +class TrackConfirm(USPSService): + """ + Calculator for domestic shipping rates + """ + SERVICE_NAME = 'Track' + CHILD_XML_NAME = 'TrackID' + API = 'TrackV2' + + def make_xml(self, userid, data): + root = ET.Element(self.SERVICE_NAME+'Request') + root.attrib['USERID'] = userid + + for data_dict in data: + track_id = data.get('ID', False) + if track_id: + data_xml = ET.element('TrackID') + data_xml.attrib['ID'] = str(track_id) + root.append(data_xml) + + return root + \ No newline at end of file diff --git a/usps/utils.py b/usps/utils.py index 21bb13a..11a9435 100644 --- a/usps/utils.py +++ b/usps/utils.py @@ -12,13 +12,14 @@ def utf8urlencode(data): utf8 URL encode the given dictionary's data @param data: a dictionary of data to be encoded + @return: the dictionary data with values URL encoded """ ret = dict() for key, value in data.iteritems(): ret[key] = value.encode('utf8') return urllib.urlencode(ret) -def dicttoxml(dictionary, tagname, parent=None, attributes=None): +def dicttoxml(dictionary, tagname, attributes=None): """ Transform a dictionary to xml @@ -26,10 +27,7 @@ def dicttoxml(dictionary, tagname, parent=None, attributes=None): @param parent: a parent node @return: XML serialization of the given dictionary """ - if parent: - element = ET.SubElement(parent, tagname) - else: - element = ET.Element(tagname) + element = ET.Element(tagname) if attributes: #USPS likes things in a certain order! for key in attributes: @@ -53,8 +51,10 @@ def xmltodict(element): Transform an xml fragment into a python dictionary @param element: an XML fragment + @return: a dictionary representation of an XML fragment """ ret = dict() for item in element.getchildren(): ret[item.tag] = item.text - return ret \ No newline at end of file + return ret + From 73139940af191a2dde2c449b93a3b15027086f28 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Thu, 29 Jul 2010 09:18:18 -0700 Subject: [PATCH 05/11] better docstrings --- usps/api/base.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/usps/api/base.py b/usps/api/base.py index c011f1d..007ac73 100644 --- a/usps/api/base.py +++ b/usps/api/base.py @@ -1,3 +1,7 @@ +""" +Base implementation of USPS service wrapper +""" + import urllib, urllib2 from usps.utils import utf8urlencode, xmltodict, dicttoxml from usps.errors import USPSXMLError @@ -8,6 +12,9 @@ from elementtree import ElementTree as ET class USPSService(object): + """ + Base USPS Service Wrapper implementation + """ SERVICE_NAME = '' API = '' CHILD_XML_NAME = '' @@ -17,6 +24,11 @@ def __init__(self, url): self.url = url def submit_xml(self, xml): + """ + submit XML to USPS + @param xml: the xml to submit + @return: the response element from USPS + """ data = {'XML':ET.tostring(xml), 'API':self.API} response = urllib2.urlopen(self.url, utf8urlencode(data)) @@ -29,12 +41,23 @@ def submit_xml(self, xml): return root def parse_xml(self, xml): + """ + Parse the response from USPS into a dictionar + @param xml: the xml to parse + @return: a dictionary representing the XML response from USPS + """ items = list() for item in xml.getchildren():#xml.findall(self.SERVICE_NAME+'Response'): items.append(xmltodict(item)) return items def make_xml(self, userid, data): + """ + Transform the data provided to an XML fragment + @param userid: the USPS API user id + @param data: the data to serialize and send to USPS + @return: an XML fragment representing data + """ root = ET.Element(self.SERVICE_NAME+'Request') root.attrib['USERID'] = userid index = 0 @@ -47,5 +70,13 @@ def make_xml(self, userid, data): return root def execute(self, userid, data): + """ + Create XML from data dictionary, submit it to + the USPS API and parse the response + + @param userid: a USPS user id + @param data: the data to serialize and submit + @return: the response from USPS as a dictionary + """ xml = self.make_xml(userid, data) return self.parse_xml(self.submit_xml(xml)) \ No newline at end of file From 7e11a92ba2ed46905da9b2489baac9f6259d0f18 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Thu, 29 Jul 2010 14:00:47 -0700 Subject: [PATCH 06/11] add service standards api and tests, make xmltodict recursive --- tests/tests.py | 215 ++++++++++++++++++++++++++++++++++- usps/api/base.py | 2 +- usps/api/servicestandards.py | 71 ++++++++++++ usps/api/tracking.py | 5 +- usps/utils.py | 12 +- 5 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 usps/api/servicestandards.py diff --git a/tests/tests.py b/tests/tests.py index 2f484ce..e5714f6 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,10 +1,215 @@ +""" +""" +from pprint import PrettyPrinter import unittest -from usps.api import USPS_CONNECTION_TEST +from usps.api import USPS_CONNECTION_TEST, USPS_CONNECTION from usps.api.addressinformation import AddressValidate, ZipCodeLookup, CityStateLookup +from usps.api.ratecalculator import DomesticRateCalculator, InternationalRateCalculator +from usps.api.servicestandards import PriorityMailServiceStandards, PackageServicesServiceStandards, ExpressMailServiceCommitment +from usps.api.tracking import TrackConfirm USERID = None +class TestRateCalculatorAPI(unittest.TestCase): + """ + Tests for Rate Calculator API wrappers + + @todo - get these tests fixed -- the test server doesn't respond to rate calculator V3 requests + yet. E-mail is out to USPS customer service to turn on production access + """ + def test_domestic_rate(self): + """ connector = DomesticRateCalculator(USPS_CONNECTION) + response = connector.execute(USERID, [{'Service': 'FIRST_CLASS', + 'FirstClassMailType': 'LETTER', + 'ZipOrigination': '44106', + 'ZipDestination': '20770', + 'Pounds': '0', + 'Ounces': '3.5', + 'Size': 'REGULAR', + 'Machinable': 'true' + }, + { + 'Service': 'PRIORITY', + 'ZipOrigination': '44106', + 'ZipDestination': '20770', + 'Pounds': '1', + 'Ounces': '8', + 'Container': 'NONRECTANGULAR', + 'Size': 'LARGE', + 'Width': '15', + 'Length': '30', + 'Height': '15', + 'Girth': '55' + }, + {'Service': 'ALL', + 'FirstClassMailType': 'LETTER', + 'ZipOrigination': '90210', + 'ZipDestination': '92298', + 'Pounds': '8', + 'Ounces': '32', + 'Container': None, + 'Size': 'REGULAR', + 'Machinable': 'true'} + ]) + """ + pass + + + + + def test_international_rate(self): + """ + connector = InternationalRateCalculator(USPS_CONNECTION) + response = connector.execute(USERID, [{ + 'Pounds': '3', + 'Ounces': '3', + 'Machinable': 'false', + 'MailType': 'Envelope', + 'Country': 'Canada', + }, + {'Pounds': '4', + 'Ounces': '3', + 'MailType': 'Package', + 'GXG': { + 'Length': '46', + 'Width': '14', + 'Height': '15', + 'POBoxFlag': 'N', + 'GiftFlag': 'N' + }, + 'ValueOfContents': '250', + 'Country': 'Japan' + }]) + """ + pass + + +class TestServiceStandardsAPI(unittest.TestCase): + """ + Tests for service standards API wrappers + """ + def test_priority_service_standards(self): + connector = PriorityMailServiceStandards(USPS_CONNECTION_TEST) + response = connector.execute(USERID, [{ + 'OriginZip': '4', + 'DestinationZip': '4' + }])[0] + self.assertEqual(response['OriginZip'], '4') + self.assertEqual(response['DestinationZip'], '4') + self.assertEqual(response['Days'], '1') + + response = connector.execute(USERID, [{ + 'OriginZip': '4', + 'DestinationZip': '5' + }])[0] + + self.assertEqual(response['OriginZip'], '4') + self.assertEqual(response['DestinationZip'], '5') + self.assertEqual(response['Days'], '2') + + def test_package_service_standards(self): + connector = PackageServicesServiceStandards(USPS_CONNECTION_TEST) + response = connector.execute(USERID, [{ + 'OriginZip': '4', + 'DestinationZip': '4' + }])[0] + + self.assertEqual(response['OriginZip'], '4') + self.assertEqual(response['DestinationZip'], '4') + self.assertEqual(response['Days'], '2') + + response = connector.execute(USERID, [{ + 'OriginZip': '4', + 'DestinationZip': '600' + }])[0] + + self.assertEqual(response['OriginZip'], '4') + self.assertEqual(response['DestinationZip'], '600') + self.assertEqual(response['Days'], '3') + + def test_express_service_commitment(self): + connector = ExpressMailServiceCommitment(USPS_CONNECTION_TEST) + response = connector.execute(USERID, [{ + 'OriginZIP': '20770', + 'DestinationZIP': '11210', + 'Date': '05-Aug-2004' + }])[0] + + self.assertEqual(response, { + 'DestinationCity': 'BROOKLYN', + 'OriginState': 'MD', + 'DestinationState': 'NY', + 'OriginZIP': '20770', + 'DestinationZIP': '11210', + 'Commitment': {'CommitmentTime': '12:00 PM', + 'CommitmentName': 'Next Day', + 'CommitmentSequence': 'A0112', + 'Location': {'City': 'BALTIMORE', + 'Zip': '21240', + 'CutOff': '9:45 PM', + 'Facility': 'AIR MAIL FACILITY', + 'State': 'MD', + 'Street': 'ROUTE 170 BLDG C DOOR 19'} + }, + 'Time': '11:30 AM', + 'Date': '05-Aug-2004', + 'OriginCity': 'GREENBELT'}) + + response = connector.execute(USERID, [{ + 'OriginZIP': '207', + 'DestinationZIP': '11210', + 'Date': '' + }])[0] + + self.assertEqual(response, { + 'Commitment': {'CommitmentName': 'Next Day', + 'CommitmentSequence': 'A0115', + 'CommitmentTime': '3:00 PM', + 'Location': {'City': 'GREENBELT', + 'CutOff': '3:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '7500 GREENWAY CENTER DRIVE', + 'Zip': '20770'} + }, + 'Date': '05-Aug-2004', + 'DestinationCity': 'BROOKLYN', + 'DestinationState': 'NY', + 'DestinationZIP': '11210', + 'OriginCity': 'GREENBELT', + 'OriginState': 'MD', + 'OriginZIP': '207', + 'Time': '11:30 AM'}) + + + +class TestTrackConfirmAPI(unittest.TestCase): + """ + Tests for Track/Confirm API wrapper + """ + def test_tracking(self): + """ + Test Track/Confirm API connector + """ + connector = TrackConfirm(USPS_CONNECTION_TEST) + response = connector.execute(USERID, [{'ID':'EJ958083578US'},])[0] + + self.assertEqual(response['TrackSummary'], 'Your item was delivered at 8:10 am on June 1 in Wilmington DE 19801.') + self.assertEqual(response['TrackDetail'][0], 'May 30 11:07 am NOTICE LEFT WILMINGTON DE 19801.') + self.assertEqual(response['TrackDetail'][1], 'May 30 10:08 am ARRIVAL AT UNIT WILMINGTON DE 19850.') + self.assertEqual(response['TrackDetail'][2], 'May 29 9:55 am ACCEPT OR PICKUP EDGEWATER NJ 07020.') + + response = connector.execute(USERID, [{'ID': 'EJ958088694US'}])[0] + self.assertEqual(response['TrackSummary'], 'Your item was delivered at 1:39 pm on June 1 in WOBURN MA 01815.') + self.assertEqual(response['TrackDetail'][0], 'May 30 7:44 am NOTICE LEFT WOBURN MA 01815.') + self.assertEqual(response['TrackDetail'][1], 'May 30 7:36 am ARRIVAL AT UNIT NORTH READING MA 01889.') + self.assertEqual(response['TrackDetail'][2], 'May 29 6:00 pm ACCEPT OR PICKUP PORTSMOUTH NH 03801.') + + class TestAddressInformationAPI(unittest.TestCase): + """ + Tests for address lookup and validation services + """ def test_address_validate(self): connector = AddressValidate(USPS_CONNECTION_TEST) response = connector.execute(USERID, [{'Address2':'6406 Ivy Lane', @@ -62,5 +267,9 @@ def test_city_state_lookup(self): if __name__ == '__main__': #please append your USPS USERID to test against the wire import sys - USERID = sys.argv.pop() - unittest.main() + if len(sys.argv) < 1: + print "You must provide a USERID" + exit() + else: + USERID = sys.argv.pop() + unittest.main() diff --git a/usps/api/base.py b/usps/api/base.py index 007ac73..0b565df 100644 --- a/usps/api/base.py +++ b/usps/api/base.py @@ -42,7 +42,7 @@ def submit_xml(self, xml): def parse_xml(self, xml): """ - Parse the response from USPS into a dictionar + Parse the response from USPS into a dictionary @param xml: the xml to parse @return: a dictionary representing the XML response from USPS """ diff --git a/usps/api/servicestandards.py b/usps/api/servicestandards.py new file mode 100644 index 0000000..e9e6a22 --- /dev/null +++ b/usps/api/servicestandards.py @@ -0,0 +1,71 @@ +""" +Service standards API wrappers +""" +from usps.utils import dicttoxml, xmltodict +from usps.api.base import USPSService + +try: + from xml.etree import ElementTree as ET +except ImportError: + from elementtree import ElementTree as ET + +class ServiceStandards(USPSService): + SERVICE_NAME = '' + API = SERVICE_NAME + PARAMETERS = [ + 'OriginZip', + 'DestinationZip' + ] + + def make_xml(self, userid, data): + """ + Transform the data provided to an XML fragment + @param userid: the USPS API user id + @param data: the data to serialize and send to USPS + @return: an XML fragment representing data + """ + for data_dict in data: + data_xml = dicttoxml(data_dict, self.SERVICE_NAME+'Request', self.PARAMETERS) + data_xml.attrib['USERID'] = userid + return data_xml + + def parse_xml(self, xml): + """ + Parse the response from USPS into a dictionary + @param xml: the xml to parse + @return: a dictionary representing the XML response from USPS + """ + return [xmltodict(xml),] + + +class PriorityMailServiceStandards(ServiceStandards): + """ + Provides shipping time estimates for Priority mail shipping methods + """ + SERVICE_NAME = 'PriorityMail' + API = SERVICE_NAME + + +class PackageServicesServiceStandards(ServiceStandards): + """ + Provides shipping time estimates for Package Services (Parcel Post, Bound Printed Matter, Library Mail, and Media Mail) + """ + SERVICE_NAME = 'StandardB' + API = SERVICE_NAME + + +class ExpressMailServiceCommitment(ServiceStandards): + """ + Provides drop off locations and commitments for shipment on a given date + """ + SERVICE_NAME = 'ExpressMailCommitment' + API = SERVICE_NAME + PARAMETERS = [ + 'OriginZIP', + 'DestinationZIP', + 'Date' + ] + + + + diff --git a/usps/api/tracking.py b/usps/api/tracking.py index 7442be7..dd3b40e 100644 --- a/usps/api/tracking.py +++ b/usps/api/tracking.py @@ -21,11 +21,12 @@ def make_xml(self, userid, data): root.attrib['USERID'] = userid for data_dict in data: - track_id = data.get('ID', False) + track_id = data_dict.get('ID', False) if track_id: - data_xml = ET.element('TrackID') + data_xml = ET.Element('TrackID') data_xml.attrib['ID'] = str(track_id) root.append(data_xml) return root + \ No newline at end of file diff --git a/usps/utils.py b/usps/utils.py index 11a9435..f5d8123 100644 --- a/usps/utils.py +++ b/usps/utils.py @@ -54,7 +54,15 @@ def xmltodict(element): @return: a dictionary representation of an XML fragment """ ret = dict() - for item in element.getchildren(): - ret[item.tag] = item.text + for item in element: + if len(item) > 0: + ret[item.tag] = xmltodict(item) + elif item.tag in ret and type(ret[item.tag]).__name__ != 'list': + val = ret.get(item.tag, None) + ret[item.tag] = [val,item.text,] + elif item.tag in ret and type(ret[item.tag]).__name__ == 'list': + ret[item.tag].append(item.text) + else: + ret[item.tag] = item.text return ret From 872a84ede57604c25f2fdec7c24982892d7a8a6c Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Thu, 29 Jul 2010 14:54:03 -0700 Subject: [PATCH 07/11] response to review --- tests/tests.py | 112 +++++++++++++++++---------------- usps/api/addressinformation.py | 15 +++-- usps/api/base.py | 23 ++++--- usps/api/ratecalculator.py | 4 +- usps/api/servicestandards.py | 12 ++-- usps/api/tracking.py | 5 +- 6 files changed, 93 insertions(+), 78 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index e5714f6..0771dd4 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -18,8 +18,8 @@ class TestRateCalculatorAPI(unittest.TestCase): yet. E-mail is out to USPS customer service to turn on production access """ def test_domestic_rate(self): - """ connector = DomesticRateCalculator(USPS_CONNECTION) - response = connector.execute(USERID, [{'Service': 'FIRST_CLASS', + """ connector = DomesticRateCalculator(USPS_CONNECTION, USERID) + response = connector.execute([{'Service': 'FIRST_CLASS', 'FirstClassMailType': 'LETTER', 'ZipOrigination': '44106', 'ZipDestination': '20770', @@ -59,8 +59,8 @@ def test_domestic_rate(self): def test_international_rate(self): """ - connector = InternationalRateCalculator(USPS_CONNECTION) - response = connector.execute(USERID, [{ + connector = InternationalRateCalculator(USPS_CONNECTION, USERID) + response = connector.execute([{ 'Pounds': '3', 'Ounces': '3', 'Machinable': 'false', @@ -89,51 +89,49 @@ class TestServiceStandardsAPI(unittest.TestCase): Tests for service standards API wrappers """ def test_priority_service_standards(self): - connector = PriorityMailServiceStandards(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{ - 'OriginZip': '4', - 'DestinationZip': '4' - }])[0] + connector = PriorityMailServiceStandards(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'OriginZip': '4', + 'DestinationZip': '4' + }])[0] + + self.assertEqual(response['OriginZip'], '4') self.assertEqual(response['DestinationZip'], '4') self.assertEqual(response['Days'], '1') - response = connector.execute(USERID, [{ - 'OriginZip': '4', - 'DestinationZip': '5' - }])[0] + response = connector.execute([{'OriginZip': '4', + 'DestinationZip': '5' + }])[0] self.assertEqual(response['OriginZip'], '4') self.assertEqual(response['DestinationZip'], '5') self.assertEqual(response['Days'], '2') def test_package_service_standards(self): - connector = PackageServicesServiceStandards(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{ - 'OriginZip': '4', - 'DestinationZip': '4' - }])[0] + connector = PackageServicesServiceStandards(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'OriginZip': '4', + 'DestinationZip': '4' + }])[0] self.assertEqual(response['OriginZip'], '4') self.assertEqual(response['DestinationZip'], '4') self.assertEqual(response['Days'], '2') - response = connector.execute(USERID, [{ - 'OriginZip': '4', - 'DestinationZip': '600' - }])[0] + response = connector.execute([{'OriginZip': '4', + 'DestinationZip': '600' + }])[0] self.assertEqual(response['OriginZip'], '4') self.assertEqual(response['DestinationZip'], '600') self.assertEqual(response['Days'], '3') def test_express_service_commitment(self): - connector = ExpressMailServiceCommitment(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{ - 'OriginZIP': '20770', - 'DestinationZIP': '11210', - 'Date': '05-Aug-2004' - }])[0] + connector = ExpressMailServiceCommitment(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{ + 'OriginZIP': '20770', + 'DestinationZIP': '11210', + 'Date': '05-Aug-2004' + }])[0] self.assertEqual(response, { 'DestinationCity': 'BROOKLYN', @@ -155,11 +153,10 @@ def test_express_service_commitment(self): 'Date': '05-Aug-2004', 'OriginCity': 'GREENBELT'}) - response = connector.execute(USERID, [{ - 'OriginZIP': '207', - 'DestinationZIP': '11210', - 'Date': '' - }])[0] + response = connector.execute([{'OriginZIP': '207', + 'DestinationZIP': '11210', + 'Date': '' + }])[0] self.assertEqual(response, { 'Commitment': {'CommitmentName': 'Next Day', @@ -191,15 +188,15 @@ def test_tracking(self): """ Test Track/Confirm API connector """ - connector = TrackConfirm(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{'ID':'EJ958083578US'},])[0] + connector = TrackConfirm(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'ID':'EJ958083578US'},])[0] self.assertEqual(response['TrackSummary'], 'Your item was delivered at 8:10 am on June 1 in Wilmington DE 19801.') self.assertEqual(response['TrackDetail'][0], 'May 30 11:07 am NOTICE LEFT WILMINGTON DE 19801.') self.assertEqual(response['TrackDetail'][1], 'May 30 10:08 am ARRIVAL AT UNIT WILMINGTON DE 19850.') self.assertEqual(response['TrackDetail'][2], 'May 29 9:55 am ACCEPT OR PICKUP EDGEWATER NJ 07020.') - response = connector.execute(USERID, [{'ID': 'EJ958088694US'}])[0] + response = connector.execute([{'ID': 'EJ958088694US'}])[0] self.assertEqual(response['TrackSummary'], 'Your item was delivered at 1:39 pm on June 1 in WOBURN MA 01815.') self.assertEqual(response['TrackDetail'][0], 'May 30 7:44 am NOTICE LEFT WOBURN MA 01815.') self.assertEqual(response['TrackDetail'][1], 'May 30 7:36 am ARRIVAL AT UNIT NORTH READING MA 01889.') @@ -211,20 +208,22 @@ class TestAddressInformationAPI(unittest.TestCase): Tests for address lookup and validation services """ def test_address_validate(self): - connector = AddressValidate(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{'Address2':'6406 Ivy Lane', - 'City':'Greenbelt', - 'State':'MD'}])[0] + connector = AddressValidate(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'Address2':'6406 Ivy Lane', + 'City':'Greenbelt', + 'State':'MD'}])[0] + self.assertEqual(response['Address2'], '6406 IVY LN') self.assertEqual(response['City'], 'GREENBELT') self.assertEqual(response['State'], 'MD') self.assertEqual(response['Zip5'], '20770') self.assertEqual(response['Zip4'], '1440') - response = connector.execute(USERID, [{'Address2':'8 Wildwood Drive', - 'City':'Old Lyme', - 'State':'CT', - 'Zip5':'06371',}])[0] + response = connector.execute([{'Address2':'8 Wildwood Drive', + 'City':'Old Lyme', + 'State':'CT', + 'Zip5':'06371',}])[0] + self.assertEqual(response['Address2'], '8 WILDWOOD DR') self.assertEqual(response['City'], 'OLD LYME') self.assertEqual(response['State'], 'CT') @@ -232,20 +231,22 @@ def test_address_validate(self): self.assertEqual(response['Zip4'], '1844') def test_zip_code_lookup(self): - connector = ZipCodeLookup(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{'Address2':'6406 Ivy Lane', - 'City':'Greenbelt', - 'State':'MD'}])[0] + connector = ZipCodeLookup(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'Address2':'6406 Ivy Lane', + 'City':'Greenbelt', + 'State':'MD'}])[0] + self.assertEqual(response['Address2'], '6406 IVY LN') self.assertEqual(response['City'], 'GREENBELT') self.assertEqual(response['State'], 'MD') self.assertEqual(response['Zip5'], '20770') self.assertEqual(response['Zip4'], '1440') - response = connector.execute(USERID, [{'Address2':'8 Wildwood Drive', - 'City':'Old Lyme', - 'State':'CT', - 'Zip5':'06371',}])[0] + response = connector.execute([{'Address2':'8 Wildwood Drive', + 'City':'Old Lyme', + 'State':'CT', + 'Zip5':'06371',}])[0] + self.assertEqual(response['Address2'], '8 WILDWOOD DR') self.assertEqual(response['City'], 'OLD LYME') self.assertEqual(response['State'], 'CT') @@ -253,13 +254,14 @@ def test_zip_code_lookup(self): self.assertEqual(response['Zip4'], '1844') def test_city_state_lookup(self): - connector = CityStateLookup(USPS_CONNECTION_TEST) - response = connector.execute(USERID, [{'Zip5':'90210'}])[0] + connector = CityStateLookup(USPS_CONNECTION_TEST, USERID) + response = connector.execute([{'Zip5':'90210'}])[0] + self.assertEqual(response['City'], 'BEVERLY HILLS') self.assertEqual(response['State'], 'CA') self.assertEqual(response['Zip5'], '90210') - response = connector.execute(USERID, [{'Zip5':'20770',}])[0] + response = connector.execute([{'Zip5':'20770',}])[0] self.assertEqual(response['City'], 'GREENBELT') self.assertEqual(response['State'], 'MD') self.assertEqual(response['Zip5'], '20770') diff --git a/usps/api/addressinformation.py b/usps/api/addressinformation.py index 975de60..9b37688 100644 --- a/usps/api/addressinformation.py +++ b/usps/api/addressinformation.py @@ -6,7 +6,6 @@ class AddressValidate(USPSService): SERVICE_NAME = 'AddressValidate' CHILD_XML_NAME = 'Address' - API = 'Verify' PARAMETERS = ['FirmName', 'Address1', 'Address2', @@ -15,19 +14,27 @@ class AddressValidate(USPSService): 'Zip5', 'Zip4',] + @property + def API(self): + return 'Verify' + class ZipCodeLookup(USPSService): SERVICE_NAME = 'ZipCodeLookup' CHILD_XML_NAME = 'Address' - API = 'ZipCodeLookup' PARAMETERS = ['FirmName', 'Address1', 'Address2', 'City', - 'State',] + 'State',] + @property + def API(self): + return 'ZipCodeLookup' class CityStateLookup(USPSService): SERVICE_NAME = 'CityStateLookup' CHILD_XML_NAME = 'ZipCode' - API = 'CityStateLookup' PARAMETERS = ['Zip5',] + @property + def API(self): + return 'CityStateLookup' diff --git a/usps/api/base.py b/usps/api/base.py index 0b565df..62b44fc 100644 --- a/usps/api/base.py +++ b/usps/api/base.py @@ -16,12 +16,16 @@ class USPSService(object): Base USPS Service Wrapper implementation """ SERVICE_NAME = '' - API = '' CHILD_XML_NAME = '' PARAMETERS = [] - def __init__(self, url): + @property + def API(self): + return self.SERVICE_NAME + + def __init__(self, url, user_id): self.url = url + self.user_id = user_id def submit_xml(self, xml): """ @@ -51,15 +55,15 @@ def parse_xml(self, xml): items.append(xmltodict(item)) return items - def make_xml(self, userid, data): + def make_xml(self, data, user_id): """ Transform the data provided to an XML fragment @param userid: the USPS API user id @param data: the data to serialize and send to USPS @return: an XML fragment representing data - """ + """ root = ET.Element(self.SERVICE_NAME+'Request') - root.attrib['USERID'] = userid + root.attrib['USERID'] = user_id index = 0 for data_dict in data: data_xml = dicttoxml(data_dict, self.CHILD_XML_NAME, self.PARAMETERS) @@ -69,14 +73,17 @@ def make_xml(self, userid, data): index += 1 return root - def execute(self, userid, data): + def execute(self,data, user_id=None): """ Create XML from data dictionary, submit it to the USPS API and parse the response - @param userid: a USPS user id + @param user_id: a USPS user id @param data: the data to serialize and submit @return: the response from USPS as a dictionary """ - xml = self.make_xml(userid, data) + if user_id is None: + user_id = self.user_id + + xml = self.make_xml(data, user_id) return self.parse_xml(self.submit_xml(xml)) \ No newline at end of file diff --git a/usps/api/ratecalculator.py b/usps/api/ratecalculator.py index 3015ab6..86d53dd 100644 --- a/usps/api/ratecalculator.py +++ b/usps/api/ratecalculator.py @@ -9,7 +9,6 @@ class DomesticRateCalculator(USPSService): """ SERVICE_NAME = 'RateV3' CHILD_XML_NAME = 'Package' - API = 'RateV3' PARAMETERS = ['Service', 'FirstClassMailType', 'ZipOrigination', @@ -34,9 +33,8 @@ class InternationalRateCalculator(USPSService): @todo - deal with nested GXG data (Length,Width,Height,POBoxFlag,GiftFlag) """ - SERVICE_NAME = 'IntlRateRequest' + SERVICE_NAME = 'IntlRate' CHILD_XML_NAME = 'Package' - API = 'IntlRate' PARAMETERS = [ 'Pounds', 'Ounces', diff --git a/usps/api/servicestandards.py b/usps/api/servicestandards.py index e9e6a22..04c4da7 100644 --- a/usps/api/servicestandards.py +++ b/usps/api/servicestandards.py @@ -11,13 +11,16 @@ class ServiceStandards(USPSService): SERVICE_NAME = '' - API = SERVICE_NAME PARAMETERS = [ 'OriginZip', 'DestinationZip' ] - def make_xml(self, userid, data): + @property + def API(self): + return self.SERVICE_NAME + + def make_xml(self, data, user_id): """ Transform the data provided to an XML fragment @param userid: the USPS API user id @@ -26,7 +29,7 @@ def make_xml(self, userid, data): """ for data_dict in data: data_xml = dicttoxml(data_dict, self.SERVICE_NAME+'Request', self.PARAMETERS) - data_xml.attrib['USERID'] = userid + data_xml.attrib['USERID'] = user_id return data_xml def parse_xml(self, xml): @@ -43,7 +46,6 @@ class PriorityMailServiceStandards(ServiceStandards): Provides shipping time estimates for Priority mail shipping methods """ SERVICE_NAME = 'PriorityMail' - API = SERVICE_NAME class PackageServicesServiceStandards(ServiceStandards): @@ -51,7 +53,6 @@ class PackageServicesServiceStandards(ServiceStandards): Provides shipping time estimates for Package Services (Parcel Post, Bound Printed Matter, Library Mail, and Media Mail) """ SERVICE_NAME = 'StandardB' - API = SERVICE_NAME class ExpressMailServiceCommitment(ServiceStandards): @@ -59,7 +60,6 @@ class ExpressMailServiceCommitment(ServiceStandards): Provides drop off locations and commitments for shipment on a given date """ SERVICE_NAME = 'ExpressMailCommitment' - API = SERVICE_NAME PARAMETERS = [ 'OriginZIP', 'DestinationZIP', diff --git a/usps/api/tracking.py b/usps/api/tracking.py index dd3b40e..195837b 100644 --- a/usps/api/tracking.py +++ b/usps/api/tracking.py @@ -16,9 +16,10 @@ class TrackConfirm(USPSService): CHILD_XML_NAME = 'TrackID' API = 'TrackV2' - def make_xml(self, userid, data): + def make_xml(self, data, user_id): + root = ET.Element(self.SERVICE_NAME+'Request') - root.attrib['USERID'] = userid + root.attrib['USERID'] = user_id for data_dict in data: track_id = data_dict.get('ID', False) From 63403d06ab366a762e484fcb953f76d70bfe57ea Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Thu, 29 Jul 2010 15:09:32 -0700 Subject: [PATCH 08/11] API doesn't always need to be a method --- usps/api/addressinformation.py | 10 +--------- usps/api/servicestandards.py | 3 --- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/usps/api/addressinformation.py b/usps/api/addressinformation.py index 9b37688..b9a2d55 100644 --- a/usps/api/addressinformation.py +++ b/usps/api/addressinformation.py @@ -6,6 +6,7 @@ class AddressValidate(USPSService): SERVICE_NAME = 'AddressValidate' CHILD_XML_NAME = 'Address' + API = 'Verify' PARAMETERS = ['FirmName', 'Address1', 'Address2', @@ -14,9 +15,6 @@ class AddressValidate(USPSService): 'Zip5', 'Zip4',] - @property - def API(self): - return 'Verify' class ZipCodeLookup(USPSService): SERVICE_NAME = 'ZipCodeLookup' @@ -26,15 +24,9 @@ class ZipCodeLookup(USPSService): 'Address2', 'City', 'State',] - @property - def API(self): - return 'ZipCodeLookup' class CityStateLookup(USPSService): SERVICE_NAME = 'CityStateLookup' CHILD_XML_NAME = 'ZipCode' PARAMETERS = ['Zip5',] - @property - def API(self): - return 'CityStateLookup' diff --git a/usps/api/servicestandards.py b/usps/api/servicestandards.py index 04c4da7..1de4d8e 100644 --- a/usps/api/servicestandards.py +++ b/usps/api/servicestandards.py @@ -16,9 +16,6 @@ class ServiceStandards(USPSService): 'DestinationZip' ] - @property - def API(self): - return self.SERVICE_NAME def make_xml(self, data, user_id): """ From 4a75d15e5cb53a91cb2c259596582d65641646a9 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Mon, 2 Aug 2010 12:10:15 -0700 Subject: [PATCH 09/11] get tests passing for all APIs --- tests/tests.py | 275 +++++++++++++++++++++++-------------- usps/api/ratecalculator.py | 8 +- usps/utils.py | 21 +-- 3 files changed, 189 insertions(+), 115 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 0771dd4..0d3df6f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,6 +1,6 @@ """ +Tests for USPS API wrappers """ -from pprint import PrettyPrinter import unittest from usps.api import USPS_CONNECTION_TEST, USPS_CONNECTION from usps.api.addressinformation import AddressValidate, ZipCodeLookup, CityStateLookup @@ -13,76 +13,96 @@ class TestRateCalculatorAPI(unittest.TestCase): """ Tests for Rate Calculator API wrappers - - @todo - get these tests fixed -- the test server doesn't respond to rate calculator V3 requests - yet. E-mail is out to USPS customer service to turn on production access """ def test_domestic_rate(self): - """ connector = DomesticRateCalculator(USPS_CONNECTION, USERID) - response = connector.execute([{'Service': 'FIRST_CLASS', - 'FirstClassMailType': 'LETTER', - 'ZipOrigination': '44106', - 'ZipDestination': '20770', - 'Pounds': '0', - 'Ounces': '3.5', - 'Size': 'REGULAR', - 'Machinable': 'true' - }, - { - 'Service': 'PRIORITY', - 'ZipOrigination': '44106', - 'ZipDestination': '20770', - 'Pounds': '1', - 'Ounces': '8', - 'Container': 'NONRECTANGULAR', - 'Size': 'LARGE', - 'Width': '15', - 'Length': '30', - 'Height': '15', - 'Girth': '55' - }, - {'Service': 'ALL', - 'FirstClassMailType': 'LETTER', - 'ZipOrigination': '90210', - 'ZipDestination': '92298', - 'Pounds': '8', - 'Ounces': '32', - 'Container': None, - 'Size': 'REGULAR', - 'Machinable': 'true'} - ]) """ - pass - + Ensure that Domestic Rate Calculator returns quotes in the expected format + """ + connector = DomesticRateCalculator(USPS_CONNECTION, USERID) + response = connector.execute([{'Service': 'First Class', + 'FirstClassMailType': 'LETTER', + 'ZipOrigination': '44106', + 'ZipDestination': '97217', + 'Pounds': '0', + 'Ounces': '3.5', + 'Size': 'REGULAR', + 'Machinable': 'true' + }, + { + 'Service': 'Priority', + 'ZipOrigination': '44106', + 'ZipDestination': '97217', + 'Pounds': '1', + 'Ounces': '8', + 'Container': 'NONRECTANGULAR', + 'Size': 'LARGE', + 'Width': '15', + 'Length': '30', + 'Height': '15', + 'Girth': '55' + }, + {'Service': 'ALL', + 'FirstClassMailType': 'LETTER', + 'ZipOrigination': '90210', + 'ZipDestination': '97217', + 'Pounds': '8', + 'Ounces': '32', + 'Container': None, + 'Size': 'REGULAR', + 'Machinable': 'true' + }, + ]) + + + for rate in [response[0], response[1]]: + self.assertTrue('Postage' in rate) + self.assertTrue('Rate' in rate['Postage']) + + self.assertTrue('Postage' in response[2]) + for postage in response[2]['Postage']: + self.assertTrue('Rate' in postage) + self.assertTrue('MailService' in postage) def test_international_rate(self): """ - connector = InternationalRateCalculator(USPS_CONNECTION, USERID) - response = connector.execute([{ - 'Pounds': '3', - 'Ounces': '3', - 'Machinable': 'false', - 'MailType': 'Envelope', - 'Country': 'Canada', - }, - {'Pounds': '4', - 'Ounces': '3', - 'MailType': 'Package', - 'GXG': { - 'Length': '46', - 'Width': '14', - 'Height': '15', - 'POBoxFlag': 'N', - 'GiftFlag': 'N' - }, - 'ValueOfContents': '250', - 'Country': 'Japan' - }]) + Ensure that International Rate Calculator returns quotes in the expected format """ - pass + connector = InternationalRateCalculator(USPS_CONNECTION, USERID) + response = connector.execute([{'Pounds': '3', + 'Ounces': '3', + 'Machinable': 'false', + 'MailType': 'Envelope', + 'Country': 'Canada', + }, + {'Pounds': '4', + 'Ounces': '3', + 'MailType': 'Package', + 'GXG': { + 'Length': '46', + 'Width': '14', + 'Height': '15', + 'POBoxFlag': 'N', + 'GiftFlag': 'N' + }, + 'ValueOfContents': '250', + 'Country': 'Japan' + }]) + + for rate in response: + self.assertTrue('Prohibitions' in rate) + self.assertTrue('Restrictions' in rate) + self.assertTrue('Observations' in rate) + self.assertTrue('CustomsForms' in rate) + self.assertTrue('ExpressMail' in rate) + self.assertTrue('AreasServed' in rate) + self.assertTrue('Service' in rate) + for service in rate['Service']: + self.assertTrue('Postage' in service) + self.assertTrue('SvcCommitments' in service) + self.assertTrue('SvcDescription' in service) class TestServiceStandardsAPI(unittest.TestCase): """ @@ -132,51 +152,87 @@ def test_express_service_commitment(self): 'DestinationZIP': '11210', 'Date': '05-Aug-2004' }])[0] + + + self.assertEqual(response, {'Commitment': [ + {'CommitmentName': 'Next Day', + 'CommitmentSequence': 'A0115', + 'CommitmentTime': '3:00 PM', + 'Location': [ + {'City': 'GREENBELT', + 'CutOff': '6:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '119 CENTER WAY', + 'Zip': '20770'}, + {'City': 'GREENBELT', + 'CutOff': '3:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '7500 GREENWAY CENTER DRIVE', + 'Zip': '20770'}]}, + {'CommitmentName': 'Next Day', + 'CommitmentSequence': 'A0112', + 'CommitmentTime': '12:00 PM', + 'Location': [ + {'City': 'GREENBELT', + 'CutOff': '6:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '119 CENTER WAY', + 'Zip': '20770'}, + {'City': 'GREENBELT', + 'CutOff': '3:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '7500 GREENWAY CENTER DRIVE', + 'Zip': '20770'}, + {'City': 'BALTIMORE', + 'CutOff': '9:45 PM', + 'Facility': 'AIR MAIL FACILITY', + 'State': 'MD', + 'Street': 'ROUTE 170 BLDG C DOOR 19', + 'Zip': '21240'} + ] + }], + 'Date': '05-Aug-2004', + 'DestinationCity': 'BROOKLYN', + 'DestinationState': 'NY', + 'DestinationZIP': '11210', + 'OriginCity': 'GREENBELT', + 'OriginState': 'MD', + 'OriginZIP': '20770', + 'Time': '11:30 AM'}) + - self.assertEqual(response, { - 'DestinationCity': 'BROOKLYN', + response = connector.execute([{'OriginZIP': '207', + 'DestinationZIP': '11210', + 'Date': '', + }])[0] + + self.assertEqual(response, {'DestinationCity': 'BROOKLYN', 'OriginState': 'MD', 'DestinationState': 'NY', - 'OriginZIP': '20770', + 'OriginZIP': '207', 'DestinationZIP': '11210', - 'Commitment': {'CommitmentTime': '12:00 PM', + 'Commitment': {'CommitmentTime': '3:00 PM', 'CommitmentName': 'Next Day', - 'CommitmentSequence': 'A0112', - 'Location': {'City': 'BALTIMORE', - 'Zip': '21240', - 'CutOff': '9:45 PM', - 'Facility': 'AIR MAIL FACILITY', - 'State': 'MD', - 'Street': 'ROUTE 170 BLDG C DOOR 19'} - }, + 'CommitmentSequence': 'A0115', + 'Location': [{'City': 'GREENBELT', + 'Zip': '20770', + 'CutOff': '6:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '119 CENTER WAY'}, + {'City': 'GREENBELT', + 'Zip': '20770', + 'CutOff': '3:00 PM', + 'Facility': 'EXPRESS MAIL COLLECTION BOX', + 'State': 'MD', + 'Street': '7500 GREENWAY CENTER DRIVE'}]}, 'Time': '11:30 AM', 'Date': '05-Aug-2004', 'OriginCity': 'GREENBELT'}) - - response = connector.execute([{'OriginZIP': '207', - 'DestinationZIP': '11210', - 'Date': '' - }])[0] - - self.assertEqual(response, { - 'Commitment': {'CommitmentName': 'Next Day', - 'CommitmentSequence': 'A0115', - 'CommitmentTime': '3:00 PM', - 'Location': {'City': 'GREENBELT', - 'CutOff': '3:00 PM', - 'Facility': 'EXPRESS MAIL COLLECTION BOX', - 'State': 'MD', - 'Street': '7500 GREENWAY CENTER DRIVE', - 'Zip': '20770'} - }, - 'Date': '05-Aug-2004', - 'DestinationCity': 'BROOKLYN', - 'DestinationState': 'NY', - 'DestinationZIP': '11210', - 'OriginCity': 'GREENBELT', - 'OriginState': 'MD', - 'OriginZIP': '207', - 'Time': '11:30 AM'}) @@ -209,9 +265,13 @@ class TestAddressInformationAPI(unittest.TestCase): """ def test_address_validate(self): connector = AddressValidate(USPS_CONNECTION_TEST, USERID) - response = connector.execute([{'Address2':'6406 Ivy Lane', + response = connector.execute([{'Firmname': '', + 'Address1': '', + 'Address2':'6406 Ivy Lane', 'City':'Greenbelt', - 'State':'MD'}])[0] + 'State':'MD', + 'Zip5': '', + 'Zip4': ''}])[0] self.assertEqual(response['Address2'], '6406 IVY LN') self.assertEqual(response['City'], 'GREENBELT') @@ -219,10 +279,13 @@ def test_address_validate(self): self.assertEqual(response['Zip5'], '20770') self.assertEqual(response['Zip4'], '1440') - response = connector.execute([{'Address2':'8 Wildwood Drive', + response = connector.execute([{'Firmname': '', + 'Address1': '', + 'Address2':'8 Wildwood Drive', 'City':'Old Lyme', 'State':'CT', - 'Zip5':'06371',}])[0] + 'Zip5':'06371', + 'Zip4': ''}])[0] self.assertEqual(response['Address2'], '8 WILDWOOD DR') self.assertEqual(response['City'], 'OLD LYME') @@ -232,7 +295,9 @@ def test_address_validate(self): def test_zip_code_lookup(self): connector = ZipCodeLookup(USPS_CONNECTION_TEST, USERID) - response = connector.execute([{'Address2':'6406 Ivy Lane', + response = connector.execute([{'Firmname': '', + 'Address1': '', + 'Address2':'6406 Ivy Lane', 'City':'Greenbelt', 'State':'MD'}])[0] @@ -242,7 +307,9 @@ def test_zip_code_lookup(self): self.assertEqual(response['Zip5'], '20770') self.assertEqual(response['Zip4'], '1440') - response = connector.execute([{'Address2':'8 Wildwood Drive', + response = connector.execute([{'Firmname': '', + 'Address1': '', + 'Address2':'8 Wildwood Drive', 'City':'Old Lyme', 'State':'CT', 'Zip5':'06371',}])[0] diff --git a/usps/api/ratecalculator.py b/usps/api/ratecalculator.py index 86d53dd..933a100 100644 --- a/usps/api/ratecalculator.py +++ b/usps/api/ratecalculator.py @@ -9,6 +9,7 @@ class DomesticRateCalculator(USPSService): """ SERVICE_NAME = 'RateV3' CHILD_XML_NAME = 'Package' + PARAMETERS = ['Service', 'FirstClassMailType', 'ZipOrigination', @@ -30,8 +31,6 @@ class DomesticRateCalculator(USPSService): class InternationalRateCalculator(USPSService): """ Calculator for international shipping rates - - @todo - deal with nested GXG data (Length,Width,Height,POBoxFlag,GiftFlag) """ SERVICE_NAME = 'IntlRate' CHILD_XML_NAME = 'Package' @@ -41,6 +40,11 @@ class InternationalRateCalculator(USPSService): 'Machinable', 'MailType', 'GXG', + 'Length', + 'Width', + 'Height', + 'POBoxFlag', + 'GiftFlag', 'ValueOfContents', 'Country', ] \ No newline at end of file diff --git a/usps/utils.py b/usps/utils.py index f5d8123..9154a4c 100644 --- a/usps/utils.py +++ b/usps/utils.py @@ -31,11 +31,11 @@ def dicttoxml(dictionary, tagname, attributes=None): if attributes: #USPS likes things in a certain order! for key in attributes: - value = dictionary.get(key, '') + value = dictionary.get(key, False) if type(value).__name__ == 'dict': - elem = dicttoxml(value, key) + elem = dicttoxml(value, key, attributes) element.append(elem) - else: + elif value != False: ET.SubElement(element, key).text = value else: for key, value in dictionary.iteritems(): @@ -56,13 +56,16 @@ def xmltodict(element): ret = dict() for item in element: if len(item) > 0: - ret[item.tag] = xmltodict(item) - elif item.tag in ret and type(ret[item.tag]).__name__ != 'list': - val = ret.get(item.tag, None) - ret[item.tag] = [val,item.text,] + value = xmltodict(item) + else: + value = item.text + + if item.tag in ret and type(ret[item.tag]).__name__ != 'list': + old_value = ret.get(item.tag, None) + ret[item.tag] = [old_value,value,] elif item.tag in ret and type(ret[item.tag]).__name__ == 'list': - ret[item.tag].append(item.text) + ret[item.tag].append(value) else: - ret[item.tag] = item.text + ret[item.tag] = value return ret From 3f2d5d31fa79775160201dfdcafc68c0ad5c5661 Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Wed, 4 Aug 2010 11:03:56 -0700 Subject: [PATCH 10/11] changes to setup.py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 16383c2..6274fd1 100755 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ from setuptools import setup, find_packages -VERSION = '0.0.1' +VERSION = '0.0.2' LONG_DESC = """\ -A python wrapper to the USPS api, currently only supports the address information api +A python wrapper to the USPS API """ setup(name='python-usps', version=VERSION, - description="A python wrapper to the USPS api, currently only supports the address information api", + description="A python wrapper to the USPS API", long_description=LONG_DESC, classifiers=[ 'Programming Language :: Python', @@ -26,7 +26,7 @@ maintainer_email = 'zbyte64@gmail.com', url='http://github.com/cuker/python-usps', license='New BSD License', - packages=find_packages(exclude=['ez_setup', 'usps', 'tests']), + packages=find_packages(exclude=['ez_setup']), zip_safe=False, install_requires=[ ], From 146730f25bbc7e8c270acc0e3640825f5e11229b Mon Sep 17 00:00:00 2001 From: Grant Thomas Date: Fri, 6 Aug 2010 12:43:33 -0700 Subject: [PATCH 11/11] new service standards method to return a service estimate for a given set of package data and postal shipping method code --- tests/tests.py | 20 ++++++++++++--- usps/api/servicestandards.py | 50 +++++++++++++++++++++++++++++++++++- usps/utils.py | 14 ++++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 0d3df6f..c458dd2 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -5,7 +5,7 @@ from usps.api import USPS_CONNECTION_TEST, USPS_CONNECTION from usps.api.addressinformation import AddressValidate, ZipCodeLookup, CityStateLookup from usps.api.ratecalculator import DomesticRateCalculator, InternationalRateCalculator -from usps.api.servicestandards import PriorityMailServiceStandards, PackageServicesServiceStandards, ExpressMailServiceCommitment +from usps.api.servicestandards import PriorityMailServiceStandards, PackageServicesServiceStandards, ExpressMailServiceCommitment, get_service_standards from usps.api.tracking import TrackConfirm USERID = None @@ -53,7 +53,6 @@ def test_domestic_rate(self): }, ]) - for rate in [response[0], response[1]]: self.assertTrue('Postage' in rate) self.assertTrue('Rate' in rate['Postage']) @@ -233,7 +232,22 @@ def test_express_service_commitment(self): 'Time': '11:30 AM', 'Date': '05-Aug-2004', 'OriginCity': 'GREENBELT'}) - + + def test_get_service_standards(self): + """ + Test the get_service_standards function + """ + data = {'OriginZip':'207', 'DestinationZip':'11210', 'CLASSID': 27} + delivery_time = get_service_standards(data, USPS_CONNECTION_TEST, USERID) + self.assertEqual(delivery_time, 'Next Day') + + data = {'OriginZip':'4', 'DestinationZip':'4', 'CLASSID': 0} + delivery_time = get_service_standards(data, USPS_CONNECTION_TEST, USERID) + self.assertEqual(delivery_time, '2 Days') + + data = {'OriginZip':'97222', 'DestinationZip':'90210', 'CLASSID': 99} + delivery_time = get_service_standards(data, USPS_CONNECTION_TEST, USERID) + self.assertEqual(delivery_time, False) class TestTrackConfirmAPI(unittest.TestCase): diff --git a/usps/api/servicestandards.py b/usps/api/servicestandards.py index 1de4d8e..e2eb5c6 100644 --- a/usps/api/servicestandards.py +++ b/usps/api/servicestandards.py @@ -8,6 +8,9 @@ from xml.etree import ElementTree as ET except ImportError: from elementtree import ElementTree as ET + + + class ServiceStandards(USPSService): SERVICE_NAME = '' @@ -62,7 +65,52 @@ class ExpressMailServiceCommitment(ServiceStandards): 'DestinationZIP', 'Date' ] + +CLASSID_TO_SERVICE = { + 'Priority': [0,1,12,16,17,18,19,22,28], + 'Package': [4,5,6,7], + 'Express': [2,3,13,23,25,27] + } +def get_service_standards(package_data, url, user_id): + """ + Given a package class id return the appropriate service standards api class + for calculating a domestic service standard or express mail commitment - + @param package_data: a dictionary containing OriginZip, DestinationZip, CLASSID, and optional Date keys + @param: url a URL to send api calls to + @param: user_id a valid USPS user id + @return: a service standard estimate for the provided data as a string or False + """ + classid = package_data.get('CLASSID', False) + connection = False + if classid in CLASSID_TO_SERVICE['Express']: + data = {} + data['OriginZIP'] = package_data.get('OriginZip') + data['DestinationZIP'] = package_data.get('DestinationZip') + data['Date'] = package_data.get('Date', "") + + connection = ExpressMailServiceCommitment(url, user_id) + response = connection.execute([data])[0] + + commitment = response.get('Commitment') + if type(commitment).__name__ == 'list': + delivery_time = commitment[0].get('CommitmentName', False) + else: + delivery_time = commitment.get('CommitmentName', False) + + else: + if classid in CLASSID_TO_SERVICE['Package']: + connection = PackageServicesServiceStandards(url, user_id) + elif classid in CLASSID_TO_SERVICE['Priority']: + connection = PackageServicesServiceStandards(url, user_id) + + if connection: + package_data.pop('Date', None) + response = connection.execute([package_data])[0] + delivery_time = '%s Days' % response.get('Days') + else: + delivery_time = False + + return delivery_time diff --git a/usps/utils.py b/usps/utils.py index 9154a4c..9c56954 100644 --- a/usps/utils.py +++ b/usps/utils.py @@ -23,6 +23,7 @@ def dicttoxml(dictionary, tagname, attributes=None): """ Transform a dictionary to xml + @todo: unit tests @param dictionary: a dictionary @param parent: a parent node @return: XML serialization of the given dictionary @@ -50,15 +51,23 @@ def xmltodict(element): """ Transform an xml fragment into a python dictionary + @todo: unit tests @param element: an XML fragment @return: a dictionary representation of an XML fragment """ ret = dict() for item in element: if len(item) > 0: - value = xmltodict(item) + value = xmltodict(item) + if len(item.attrib.items()) > 0: + for k, v in item.attrib.items(): + value[k] = v + elif len(item.attrib.items()) > 0: + value = {'text': item.text} + for k, v in item.attrib.items(): + value[k] = v else: - value = item.text + value = item.text if item.tag in ret and type(ret[item.tag]).__name__ != 'list': old_value = ret.get(item.tag, None) @@ -67,5 +76,6 @@ def xmltodict(element): ret[item.tag].append(value) else: ret[item.tag] = value + return ret