Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jay Goel committed Feb 21, 2013
1 parent d7cf42e commit a945511
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 4 deletions.
Empty file added ClassicUPS/__init__.py
Empty file.
288 changes: 288 additions & 0 deletions ClassicUPS/ups.py
@@ -0,0 +1,288 @@
import requests
from dict2xml import dict2xml
import xmltodict
import json
import os
import tempfile
import shutil
import urllib

from binascii import a2b_base64
from datetime import datetime
from xhtml2pdf import pisa


class UPSConnection(object):

test_urls = {
'track': 'https://wwwcie.ups.com/ups.app/xml/Track',
'ship_confirm': 'https://wwwcie.ups.com/ups.app/xml/ShipConfirm',
'ship_accept': 'https://wwwcie.ups.com/ups.app/xml/ShipAccept',
}
production_urls = {
'track': 'https://onlinetools.ups.com/ups.app/xml/Track',
'ship_confirm': 'https://onlinetools.ups.com/ups.app/xml/ShipConfirm',
'ship_accept': 'https://onlinetools.ups.com/ups.app/xml/ShipAccept',
}

def __init__(self, license_number, user_id, password, shipper_number=None,
debug=False):

self.license_number = license_number
self.user_id = user_id
self.password = password
self.shipper_number = shipper_number
self.debug = debug

def _generate_xml(self, url_action, ups_request):
access_request = {
'AccessRequest': {
'AccessLicenseNumber': self.license_number,
'UserId': self.user_id,
'Password': self.password,
}
}

xml = '''
<?xml version="1.0"?>
{access_request_xml}
<?xml version="1.0"?>
{api_xml}
'''.format(
request_type=url_action,
access_request_xml=dict2xml(access_request),
api_xml=dict2xml(ups_request),
)

return xml

def _transmit_request(self, url_action, ups_request):
url = self.production_urls[url_action]
if self.debug:
url = self.test_urls[url_action]

xml = self._generate_xml(url_action, ups_request)
resp = requests.post(url, data=xml)

return UPSResult(resp)

def tracking_info(self, *args, **kwargs):
return TrackingInfo(self, *args, **kwargs)

def create_shipment(self, *args, **kwargs):
return Shipment(self, *args, **kwargs)

class UPSResult(object):

def __init__(self, response):
self.response = response

@property
def xml_response(self):
return self.response.text

@property
def dict_response(self):
return json.loads(json.dumps(xmltodict.parse(self.xml_response)))

class TrackingInfo(object):

def __init__(self, ups_conn, tracking_number):
self.tracking_number = tracking_number

tracking_request = {
'TrackRequest': {
'Request': {
'TransactionReference': {
'CustomerContext': 'Get tracking status',
'XpciVersion': '1.0',
},
'RequestAction': 'Track',
'RequestOption': 'activity',
},
'TrackingNumber': tracking_number,
},
}

self.result = ups_conn._transmit_request('track', tracking_request)

@property
def shipment_activities(self):
shipment_activities = (self.result.dict_response['TrackResponse']
['Shipment']['Package']['Activity'])
if type(shipment_activities) != list:
shipment_activities = [shipment_activities]

return shipment_activities

@property
def delivered(self):
delivered = [x for x in self.shipment_activities
if x['Status']['StatusType']['Code'] == 'D']
if delivered:
return datetime.strptime(delivered[0]['Date'], '%Y%m%d')

class Shipment(object):

def __init__(self, ups_conn, from_addr, to_addr, dimensions, weight,
file_format='EPL', reference_number=None):

self.file_format = file_format

shipping_request = {
'ShipmentConfirmRequest': {
'Request': {
'TransactionReference': {
'CustomerContext': 'get new shipment',
'XpciVersion': '1.0001',
},
'RequestAction': 'ShipConfirm',
'RequestOption': 'nonvalidate', # TODO: what does this mean?
},
'Shipment': {
'Shipper': {
'Name': from_addr['name'],
'PhoneNumber': from_addr['phone'],
'ShipperNumber': ups_conn.shipper_number,
'Address': {
'AddressLine1': from_addr['address1'],
'City': from_addr['city'],
'StateProvinceCode': from_addr['state'],
'CountryCode': from_addr['country'],
'PostalCode': from_addr['postal_code'],
},
},
'ShipTo' : {
'CompanyName': to_addr['name'],
'AttentionName': to_addr['name'],
'PhoneNumber': to_addr['phone'],
'Address': {
'AddressLine1': to_addr['address1'],
'City': to_addr['city'],
'StateProvinceCode': to_addr['state'],
'CountryCode': to_addr['country'],
'PostalCode': to_addr['postal_code'],
'ResidentialAddress': '', # TODO: omit this if not residential
},
},
'Service' : { # TODO: add new service types
'Code': '03',
'Description': 'Ground',
},
'PaymentInformation': { # TODO: Other payment information
'Prepaid': {
'BillShipper': {
'AccountNumber': ups_conn.shipper_number,
},
},
},
'Package': {
'PackagingType': {
'Code': '02',
},
'Dimensions': {
'UnitOfMeasurement': {
'Code': 'IN',
},
'Length': dimensions['length'],
'Width': dimensions['width'],
'Height': dimensions['height'],
},
'PackageWeight': {
'Weight': weight, # Units are pounds (lbs)
},
'PackageServiceOptions': '', # TODO: insured value, verbal confirmation, etc
},
},
'LabelSpecification': { # TODO: support GIF and EPL (and others)
'LabelPrintMethod': {
'Code': file_format,
},
'LabelStockSize': {
'Width': '6',
'Height': '4',
},
'HTTPUserAgent': 'Mozilla/4.5',
'LabelImageFormat': {
'Code': 'GIF',
},
},
},
}

if from_addr.get('address2'):
shipping_request['ShipmentConfirmRequest']['Shipment']['Shipper']['Address']['AddressLine2'] = from_addr['address2']

if to_addr.get('company'):
shipping_request['ShipmentConfirmRequest']['Shipment']['ShipTo']['CompanyName'] = to_addr['company']

if to_addr.get('address2'):
shipping_request['ShipmentConfirmRequest']['Shipment']['ShipTo']['Address']['AddressLine2'] = to_addr['address2']

self.confirm_result = ups_conn._transmit_request('ship_confirm', shipping_request)

confirm_result_digest = self.confirm_result.dict_response['ShipmentConfirmResponse']['ShipmentDigest']
ship_accept_request = {
'ShipmentAcceptRequest': {
'Request': {
'TransactionReference': {
'CustomerContext': 'shipment accept reference',
'XpciVersion': '1.0001',
},
'RequestAction': 'ShipAccept',
},
'ShipmentDigest': confirm_result_digest,
}
}

self.accept_result = ups_conn._transmit_request('ship_accept', ship_accept_request)

@property
def cost(self):
total_cost = self.confirm_result.dict_response['ShipmentConfirmResponse']['ShipmentCharges']['TotalCharges']['MonetaryValue']
return float(total_cost)

@property
def tracking_number(self):
tracking_number = self.confirm_result.dict_response['ShipmentConfirmResponse']['ShipmentIdentificationNumber']
return tracking_number

def save_label(self, fd):
raw_epl = self.accept_result.dict_response['ShipmentAcceptResponse']['ShipmentResults']['PackageResults']['LabelImage']['GraphicImage']
binary = a2b_base64(raw_epl)
fd.write(binary)

def save_html(self, output_directory):
if self.file_format == 'GIF':
# TODO: if file format is not GIF some sort of exception

gif_path = os.path.join(output_directory, 'label' + self.tracking_number + '.gif')
html_path = os.path.join(output_directory, self.tracking_number + '.html')

gif_fd = open(gif_path, 'wb')
raw_gif = self.accept_result.dict_response['ShipmentAcceptResponse']['ShipmentResults']['PackageResults']['LabelImage']['GraphicImage']
binary = a2b_base64(raw_gif)
gif_fd.write(binary)
gif_fd.close()

html_fd = open(html_path, 'wb')
raw_html = self.accept_result.dict_response['ShipmentAcceptResponse']['ShipmentResults']['PackageResults']['LabelImage']['HTMLImage']
binary = a2b_base64(raw_html)
html_fd.write(binary)
html_fd.close()


def save_pdf(self, fd):
if self.file_format == 'GIF':
# TODO: if file format is not GIF some sort of exception

tmp_path = tempfile.mkdtemp()
self.save_html(tmp_path)
html_path = os.path.join('file://' + tmp_path, self.tracking_number + '.html')

pdf = pisa.CreatePDF(urllib.urlopen(html_path), fd, log_warn=1,
log_err=1, path=html_path,
link_callback=pisa.pisaLinkLoader(html_path).getFileName)

shutil.rmtree(tmp_path)
13 changes: 13 additions & 0 deletions LICENSE
@@ -0,0 +1,13 @@
Copyright 2013 Classic Specs

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
1 change: 1 addition & 0 deletions MANIFEST.in
@@ -0,0 +1 @@
include requirements.txt LICENSE
4 changes: 0 additions & 4 deletions README.md

This file was deleted.

78 changes: 78 additions & 0 deletions README.txt
@@ -0,0 +1,78 @@
ClassicUPS: A Useful UPS Library
================================

ClassicUPS is an Apache2 Licensed wrapper around the UPS API for creating
shipping labels and fetching a package's tracking status. This library by no
means encompasses all of the UPS functionality, but it is suitable for some of
the most common shipping-related common tasks.


Features
--------

- Given a tracking number, check its transit history, delivery status, and
delivery date

- Create shipping labels in GIF, PDF, HTML, or EPS (thermal printer) format


Installation
------------

Installation is easy:

$ pip install ClassicUPS


Quickstart
----------

from ClassicUPS.ups import UPSConnection

# Obtain credentials from the UPS website
ups = UPSConnection(license_number,
user_id,
password,
shipper_number, # Optional if you are not creating a shipment
debug=True) # Use the UPS sandbox API rather than prod


# Check the delivery date of a package. Returns `None` if it has not been delivered
print ups.tracking_info('1Z12345E0291980793').delivered


# Create shipment and save shipping label as GIF file
from_addr = {
'name': 'Google',
'address1': '1600 Amphitheatre Parkway',
'city': 'Mountain View',
'state': 'CA',
'country': 'US',
'postal_code': '94043',
'phone': '6502530000'
}
to_addr = {
'name': 'President',
'address1': '1600 Pennsylvania Ave',
'city': 'Washington',
'state': 'DC',
'country': 'US',
'postal_code': '20500',
'phone': '202456-1111'
}
dimensions = { # in inches
'length': 1,
'width': 4,
'height': 9
}
weight = 10 # in lbs

# Create the shipment. Use file_format='EPS' for a thermal-printer-compatible EPS
shipment = ups.create_shipment(from_addr, to_addr, dimensions, weight, file_format='GIF')

# Print information about our shipment
print shipment.cost
print shipment.tracking_number

# Save the shipping label to print, email, etc
shipment.save_label(open('label.gif', 'wb'))

0 comments on commit a945511

Please sign in to comment.