From a945511877f9bf15255b96aa496de91f6ea5cc2c Mon Sep 17 00:00:00 2001 From: Jay Goel Date: Thu, 21 Feb 2013 12:42:00 -0500 Subject: [PATCH] Initial commit --- ClassicUPS/__init__.py | 0 ClassicUPS/ups.py | 288 +++++++++++++++++++++++++++++++++++++++++ LICENSE | 13 ++ MANIFEST.in | 1 + README.md | 4 - README.txt | 78 +++++++++++ setup.py | 26 ++++ 7 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 ClassicUPS/__init__.py create mode 100644 ClassicUPS/ups.py create mode 100644 LICENSE create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.txt create mode 100644 setup.py diff --git a/ClassicUPS/__init__.py b/ClassicUPS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ClassicUPS/ups.py b/ClassicUPS/ups.py new file mode 100644 index 0000000..7303c89 --- /dev/null +++ b/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 = ''' + + {access_request_xml} + + + {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) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9b2de14 --- /dev/null +++ b/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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..656fc26 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt LICENSE diff --git a/README.md b/README.md deleted file mode 100644 index a76c503..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -ClassicUPS -========== - -Wrapper around the UPS API for creating shipping labels and fetching a package's tracking status. \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..2c20ead --- /dev/null +++ b/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')) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5950e61 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from distutils.core import setup + +setup( + name='ClassicUPS', + version='0.1.0', + author='Jay Goel', + author_email='jay@classicspecs.com', + url='http://github.com/classicspecs/ClassicUPS/', + packages=['ClassicUPS'], + description='Library integrating with the UPS API', + keywords=['UPS'], + install_requires=[ + 'dict2xml == 1.0', + 'xmltodict == 0.4.2', + 'xhtml2pdf == 0.0.4', + 'requests == 0.14.2' + ], + classifiers=[ + 'Programming Language :: Python', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Intended Audience :: Developers', + 'Development Status :: 4 - Beta' + ] +)