Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jay Goel
committed
Feb 21, 2013
1 parent
d7cf42e
commit a945511
Showing
7 changed files
with
406 additions
and
4 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include requirements.txt LICENSE |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')) |
Oops, something went wrong.