Skip to content

Instantly share code, notes, and snippets.

@dgrant
Last active July 14, 2016 16:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dgrant/5386253 to your computer and use it in GitHub Desktop.
Save dgrant/5386253 to your computer and use it in GitHub Desktop.
Finds Canada Post rates to send a parcel or oversized/non-standard envelope domestically, to the US, or internationally. Particularly useful for people selling items on eBay. It will print our rates to different destinations around the country/US/world so you can get a feel for the min/average/max rate.
#!/usr/bin/env python3
import argparse
import base64
import xml.etree.ElementTree as ET
import urllib.request, urllib.error, urllib.parse
import operator
import time
from canadapostconf import username, password
NS = '{http://www.canadapost.ca/ws/ship/rate-v2}'
RATE_POST = """
<mailing-scenario xmlns="http://www.canadapost.ca/ws/ship/rate-v2">
<quote-type>counter</quote-type>
<parcel-characteristics>
<weight>%(weight)f</weight>
<dimensions>
<length>%(length)f</length>
<width>%(width)f</width>
<height>%(height)f</height>
</dimensions>
</parcel-characteristics>
<origin-postal-code>%(origin)s</origin-postal-code>
<destination>
</destination>
</mailing-scenario>
"""
CODES = {
'united-states':
('zip-code', {
'Florida': '32792',
'Washington': '98576',
'California': '93215',
'Maine': '04453',
'Kansas': '67425',
}),
'international':
('country-code', {
'Great Britain': 'GB',
'Hungary': 'HU',
'Russia': 'RU',
'China': 'CN',
'Australia': 'AU',
'Brazil': 'BR',
}),
'domestic':
('postal-code', {
'Vancouver, BC': 'V5K0A1',
'St. John\'s, NL': 'A1A0A1',
'Toronto, ON': 'M3H6A7',
'Rainbox Lake, AB': 'T0H2Y0',
'Churchill, MB': 'R0B0E0',
'Rouyn-Noranda, QC': 'J9X1A1',
}),
}
def get_letter_rate(weight, destination_type):
if destination_type == 'domestic':
if weight <= 0.1:
rate = 1.80
elif weight <= 0.2:
rate = 2.95
elif weight <= 0.3:
rate = 4.10
elif weight <= 0.4:
rate = 4.70
elif weight <= 0.5:
rate = 5.05
elif destination_type == 'united-states':
if weight <= 0.1:
rate = 2.95
elif weight <= 0.2:
rate = 5.15
elif weight <= 0.5:
rate = 10.30
elif destination_type == 'international':
if weight <= 0.1:
rate = 5.90
elif weight <= 0.2:
rate = 10.30
elif weight <= 0.5:
rate = 20.60
return rate
def get_rate_quote(weight, length, width, height, origin, destination_type, code):
if length < width:
raise Exception('length cannot be less than width')
if width < height:
raise Exception('width cannot be less than height')
if 14 <= length and length <= 38 \
and 9 <= width and width <= 27 \
and 0 <= height and height <= 2:
print("*** Non-standard and Oversize Lettermail")
rate = get_letter_rate(weight, destination_type)
return [(rate, 'oversize and non-standard lettermail')]
post = RATE_POST % {'weight': weight, 'length': length, 'width': width, 'height': height,
'origin': origin}
root = ET.fromstring(post)
destination_element = root.find(NS + 'destination')
destination_type_elem = ET.Element(NS + destination_type)
destination_code_elem = ET.Element(NS + CODES[destination_type][0])
destination_code_elem.text = code
destination_type_elem.append(destination_code_elem)
destination_element.append(destination_type_elem)
post = ET.tostring(root)
base64auth = base64.encodestring(('%s:%s' % (username, password)).encode())[:-1].decode()
headers = {
'Accept': 'application/vnd.cpc.ship.rate-v2+xml',
'Content-Type': 'application/vnd.cpc.ship.rate-v2+xml',
'Accept-language': 'en-CA',
'Authorization': 'Basic %s' % base64auth
}
req = urllib.request.Request('https://soa-gw.canadapost.ca/rs/ship/price', post,
headers=headers)
try:
response = urllib.request.urlopen(req).read()
except urllib.error.HTTPError:
print("Error making request:", post)
print("response=", response)
raise
time.sleep(1)
quotes = []
root = ET.fromstring(response)
price_quotes = root.findall(NS + 'price-quote')
for price_quote in price_quotes:
service_name = price_quote.find(NS + 'service-name').text
price = price_quote.find(NS + 'price-details').find(NS + 'due').text
quotes.append((float(price), service_name))
# sort by lowest price
quotes = sorted(quotes, key=operator.itemgetter(0))
return quotes
def get_rate_quotes(weight, length, width, height, origin):
origin = origin.upper()
for ship_type in CODES.keys():
print("*"*20, ship_type, "*"*20)
for place, code in CODES[ship_type][1].items():
print("*** Prices for", place)
rates = get_rate_quote(weight, length, width, height, origin, ship_type, code)
for price, name in rates:
print("$%.2f (%s)" % (price, name))
def main():
parser = argparse.ArgumentParser(description='Get postal rate quotes from Canada Post')
parser.add_argument('weight', metavar='WEIGHT', type=float,
help='weight of package in kg')
parser.add_argument('length', metavar='LENGTH', type=float,
help='length of package in cm')
parser.add_argument('width', metavar='WIDTH', type=float,
help='width of package in cm')
parser.add_argument('height', metavar='HEIGHT', type=float,
help='height of package in cm')
parser.add_argument('origin', metavar='POSTAL_CODE',
help='origin postal code')
args = parser.parse_args()
get_rate_quotes(args.weight, args.length, args.width, args.height, args.origin)
if __name__ == '__main__':
main()
username = 'XXXX'
password = 'XXXX'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment